Compare commits
75 Commits
histograms
...
v2.0.0-bet
Author | SHA1 | Date | |
---|---|---|---|
|
90635ffc4e | ||
|
056f850268 | ||
|
336f2d88e9 | ||
|
e16b4062b5 | ||
|
747dacf3b1 | ||
|
f00a71922f | ||
|
c97c9d4ece | ||
|
9d3f8672d9 | ||
|
fe48cd4236 | ||
|
587d3aa612 | ||
|
8a951940fd | ||
|
b726ef8a2e | ||
|
25e360e175 | ||
|
1d9ec253fb | ||
|
3cf1aa00fa | ||
|
36a5d0ee3f | ||
|
f5e5174045 | ||
|
ba2301ebfe | ||
|
df651a2157 | ||
|
2d2c1d5f2d | ||
|
0f93581ff5 | ||
|
397452a7fe | ||
|
cd3157361a | ||
|
29a89f185a | ||
|
2f7a5c2967 | ||
|
f07ed53f7e | ||
|
7348a6a62f | ||
|
b7ba53eb60 | ||
|
e389d6a96b | ||
|
0238dffc7a | ||
|
666445e8f7 | ||
|
36bada8feb | ||
|
b4946f4db1 | ||
|
f3d485da53 | ||
|
3342122be2 | ||
|
f59751853a | ||
|
a60c55c6df | ||
|
3bad5883bb | ||
|
d4c30866b7 | ||
|
7c92ce771f | ||
|
4601359ebe | ||
|
04b1130837 | ||
|
c377617b5a | ||
|
222e8f66df | ||
|
87e2f5f414 | ||
|
7de05700e9 | ||
|
841f41da2f | ||
|
73dcde7780 | ||
|
377ff52222 | ||
|
e4b5609d78 | ||
|
c93497af10 | ||
|
99dda66bbc | ||
|
4ce68f817d | ||
|
1c027be106 | ||
|
4a94074595 | ||
|
b49f6c3f86 | ||
|
9c8fe9277d | ||
|
a7c70e6912 | ||
|
28156a5a6b | ||
|
a3cfb9e5e4 | ||
|
25ccc6a9f9 | ||
|
8d1904c185 | ||
|
85cc221d50 | ||
|
588b00de45 | ||
|
656a1f8294 | ||
|
1dd37c5020 | ||
|
3c40dc1f49 | ||
|
6d560caf06 | ||
|
07f3301e32 | ||
|
9410a293f7 | ||
|
a0704eddb0 | ||
|
672491c66c | ||
|
bd91c45d1f | ||
|
f77cdb7148 | ||
|
7878a4365c |
@@ -1,16 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\BTCPayServer.Client\BTCPayServer.Client.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="8.0.6" />
|
||||
</ItemGroup>
|
||||
</Project>
|
@@ -1,122 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Lightning;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayApp.CommonServer;
|
||||
|
||||
//methods available on the hub in the client
|
||||
public interface IBTCPayAppHubClient
|
||||
{
|
||||
Task NotifyServerEvent(ServerEvent ev);
|
||||
Task NotifyNetwork(string network);
|
||||
Task NotifyServerNode(string nodeInfo);
|
||||
Task TransactionDetected(TransactionDetectedRequest request);
|
||||
Task NewBlock(string block);
|
||||
|
||||
Task<LightningInvoice> CreateInvoice(CreateLightningInvoiceRequest createLightningInvoiceRequest);
|
||||
Task<LightningInvoice?> GetLightningInvoice(uint256 paymentHash);
|
||||
Task<LightningPayment?> GetLightningPayment(uint256 paymentHash);
|
||||
Task<List<LightningPayment>> GetLightningPayments(ListPaymentsParams request);
|
||||
Task<List<LightningInvoice>> GetLightningInvoices(ListInvoicesParams request);
|
||||
Task<PayResponse> PayInvoice(string bolt11, long? amountMilliSatoshi);
|
||||
Task MasterUpdated(long? deviceIdentifier);
|
||||
Task<LightningNodeInformation> GetLightningNodeInfo();
|
||||
Task<LightningNodeBalance> GetLightningBalance();
|
||||
}
|
||||
|
||||
//methods available on the hub in the server
|
||||
public interface IBTCPayAppHubServer
|
||||
{
|
||||
Task<bool> DeviceMasterSignal(long deviceIdentifier, bool active);
|
||||
|
||||
Task<Dictionary<string,string>> Pair(PairRequest request);
|
||||
Task<AppHandshakeResponse> Handshake(AppHandshake request);
|
||||
Task<bool> BroadcastTransaction(string tx);
|
||||
Task<decimal> GetFeeRate(int blockTarget);
|
||||
Task<BestBlockResponse> GetBestBlock();
|
||||
|
||||
Task<TxInfoResponse> FetchTxsAndTheirBlockHeads(string[] txIds);
|
||||
Task<string> DeriveScript(string identifier);
|
||||
Task TrackScripts(string identifier, string[] scripts);
|
||||
Task<string> UpdatePsbt(string[] identifiers, string psbt);
|
||||
Task<CoinResponse[]> GetUTXOs(string[] identifiers);
|
||||
Task<Dictionary<string, TxResp[]>> GetTransactions(string[] identifiers);
|
||||
|
||||
Task SendInvoiceUpdate(string identifier, LightningInvoice lightningInvoice);
|
||||
}
|
||||
|
||||
public class ServerEvent(string type)
|
||||
{
|
||||
public string Type { get; } = type;
|
||||
public string? StoreId { get; init; }
|
||||
public string? UserId { get; init; }
|
||||
public string? InvoiceId { get; init; }
|
||||
}
|
||||
|
||||
public record TxResp(long Confirmations, long? Height, decimal BalanceChange, DateTimeOffset Timestamp, string TransactionId)
|
||||
{
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{{ Confirmations = {Confirmations}, Height = {Height}, BalanceChange = {BalanceChange}, Timestamp = {Timestamp}, TransactionId = {TransactionId} }}";
|
||||
}
|
||||
}
|
||||
|
||||
public class TransactionDetectedRequest
|
||||
{
|
||||
public string Identifier { get; set; }
|
||||
public string TxId { get; set; }
|
||||
public string[] SpentScripts { get; set; }
|
||||
public string[] ReceivedScripts { get; set; }
|
||||
public bool Confirmed { get; set; }
|
||||
}
|
||||
|
||||
public class CoinResponse
|
||||
{
|
||||
public string Identifier{ get; set; }
|
||||
public bool Confirmed { get; set; }
|
||||
public string Script { get; set; }
|
||||
public string Outpoint { get; set; }
|
||||
public decimal Value { get; set; }
|
||||
public string Path { get; set; }
|
||||
}
|
||||
|
||||
public class TxInfoResponse
|
||||
{
|
||||
public Dictionary<string,TransactionResponse> Txs { get; set; }
|
||||
public Dictionary<string,string> Blocks { get; set; }
|
||||
public Dictionary<string,int> BlockHeghts { get; set; }
|
||||
}
|
||||
|
||||
public class TransactionResponse
|
||||
{
|
||||
public string? BlockHash { get; set; }
|
||||
public int? BlockHeight { get; set; }
|
||||
public string Transaction { get; set; }
|
||||
}
|
||||
|
||||
public class BestBlockResponse
|
||||
{
|
||||
public required string BlockHash { get; set; }
|
||||
public required int BlockHeight { get; set; }
|
||||
public string BlockHeader { get; set; }
|
||||
}
|
||||
|
||||
public class AppHandshake
|
||||
{
|
||||
public string[] Identifiers { get; set; }
|
||||
}
|
||||
|
||||
public class AppHandshakeResponse
|
||||
{
|
||||
//response about identifiers being tracked successfully
|
||||
public string[] IdentifiersAcknowledged { get; set; }
|
||||
}
|
||||
|
||||
|
||||
public class PairRequest
|
||||
{
|
||||
public Dictionary<string, string?> Derivations { get; set; } = new();
|
||||
}
|
@@ -1,12 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace BTCPayApp.CommonServer.Models;
|
||||
|
||||
public class AcceptInviteRequest
|
||||
{
|
||||
[Required]
|
||||
public string? UserId { get; init; }
|
||||
|
||||
[Required]
|
||||
public string? Code { get; init; }
|
||||
}
|
@@ -1,9 +0,0 @@
|
||||
namespace BTCPayApp.CommonServer.Models;
|
||||
|
||||
public class AcceptInviteResult(string email)
|
||||
{
|
||||
public string Email { get; init; } = email;
|
||||
public bool? RequiresUserApproval { get; set; }
|
||||
public bool? EmailHasBeenConfirmed { get; set; }
|
||||
public string? PasswordSetCode { get; set; }
|
||||
}
|
@@ -1,10 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace BTCPayApp.CommonServer.Models;
|
||||
|
||||
public class AccessTokenResult(string accessToken, string refreshToken, DateTimeOffset expiry)
|
||||
{
|
||||
public string AccessToken { get; init; } = accessToken;
|
||||
public string RefreshToken { get; init; } = refreshToken;
|
||||
public DateTimeOffset Expiry { get; init; } = expiry;
|
||||
}
|
@@ -1,12 +0,0 @@
|
||||
namespace BTCPayApp.CommonServer.Models;
|
||||
|
||||
public class AppInstanceInfo
|
||||
{
|
||||
public string BaseUrl { get; set; }
|
||||
public string ServerName { get; set; }
|
||||
public string? ContactUrl { get; set; }
|
||||
public string? LogoUrl { get; set; }
|
||||
public string? CustomThemeCssUrl { get; set; }
|
||||
public string? CustomThemeExtension { get; set; }
|
||||
public bool RegistrationEnabled { get; set; }
|
||||
}
|
@@ -1,68 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text.Json;
|
||||
using BTCPayServer.Lightning;
|
||||
using NBitcoin;
|
||||
using NBitcoin.JsonConverters;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayApp.CommonServer.Models;
|
||||
|
||||
// public partial class LightningPayment
|
||||
// {
|
||||
// public string PaymentHash { get; set; }
|
||||
// public string? PaymentId { get; set; }
|
||||
// public string? Preimage { get; set; }
|
||||
// public string? Secret { get; set; }
|
||||
// public bool Inbound { get; set; }
|
||||
// public DateTimeOffset Timestamp { get; set; }
|
||||
// public long Value { get; set; }
|
||||
// public LightningPaymentStatus Status { get; set; }
|
||||
//
|
||||
// //you can have multiple requests generated for the same payment hash, but once you reveal the preimage, you should reject any attempt to pay the same payment hash
|
||||
// public List<string> PaymentRequests { get; set; }
|
||||
// [JsonIgnore]
|
||||
// public Dictionary<string, JsonDocument> AdditionalData { get; set; }
|
||||
//
|
||||
// }
|
||||
|
||||
public class AppUserInfo
|
||||
{
|
||||
public string? UserId { get; set; }
|
||||
public string? Email { get; set; }
|
||||
public string? Name { get; set; }
|
||||
public string? ImageUrl { get; set; }
|
||||
public IEnumerable<string>? Roles { get; set; }
|
||||
public IEnumerable<AppUserStoreInfo>? Stores { get; set; }
|
||||
|
||||
public void SetInfo(string email, string? name, string? imageUrl)
|
||||
{
|
||||
Email = email;
|
||||
Name = name;
|
||||
ImageUrl = imageUrl;
|
||||
}
|
||||
|
||||
public static bool Equals(AppUserInfo? x, AppUserInfo? y)
|
||||
{
|
||||
if (ReferenceEquals(x, y)) return true;
|
||||
if (ReferenceEquals(x, null)) return false;
|
||||
if (ReferenceEquals(y, null)) return false;
|
||||
if (x.GetType() != y.GetType()) return false;
|
||||
return x.UserId == y.UserId && x.Email == y.Email &&
|
||||
x.Name == y.Name && x.ImageUrl == y.ImageUrl &&
|
||||
Equals(x.Roles, y.Roles) && Equals(x.Stores, y.Stores);
|
||||
}
|
||||
}
|
||||
|
||||
public class AppUserStoreInfo
|
||||
{
|
||||
public string? Id { get; set; }
|
||||
public string? Name { get; set; }
|
||||
public string? LogoUrl { get; set; }
|
||||
public string? RoleId { get; set; }
|
||||
public string? PosAppId { get; set; }
|
||||
public string? DefaultCurrency { get; set; }
|
||||
public bool Archived { get; set; }
|
||||
public IEnumerable<string>? Permissions { get; set; }
|
||||
}
|
@@ -1,10 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace BTCPayApp.CommonServer.Models;
|
||||
|
||||
public class CreateStoreData
|
||||
{
|
||||
public string? DefaultCurrency { get; set; }
|
||||
public string? RecommendedExchangeId { get; set; }
|
||||
public Dictionary<string, string>? Exchanges { get; set; }
|
||||
}
|
@@ -1,12 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace BTCPayApp.CommonServer.Models;
|
||||
|
||||
public class SignupRequest
|
||||
{
|
||||
[Required]
|
||||
public string? Email { get; init; }
|
||||
|
||||
[Required]
|
||||
public string? Password { get; init; }
|
||||
}
|
@@ -1,8 +0,0 @@
|
||||
namespace BTCPayApp.CommonServer.Models;
|
||||
|
||||
public class SignupResult
|
||||
{
|
||||
public string? Email { get; set; }
|
||||
public bool RequiresConfirmedEmail { get; set; }
|
||||
public bool RequiresUserApproval { get; set; }
|
||||
}
|
@@ -1,49 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayApp.CommonServer;
|
||||
|
||||
public static class PosDataParser
|
||||
{
|
||||
public static Dictionary<string, object> ParsePosData(JToken? posData)
|
||||
{
|
||||
var result = new Dictionary<string, object>();
|
||||
if (posData is JObject jobj)
|
||||
{
|
||||
foreach (var item in jobj)
|
||||
{
|
||||
ParsePosDataItem(item, ref result);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static void ParsePosDataItem(KeyValuePair<string, JToken?> item, ref Dictionary<string, object> result)
|
||||
{
|
||||
switch (item.Value?.Type)
|
||||
{
|
||||
case JTokenType.Array:
|
||||
var items = item.Value.AsEnumerable().ToList();
|
||||
var arrayResult = new List<object>();
|
||||
for (var i = 0; i < items.Count; i++)
|
||||
{
|
||||
arrayResult.Add(items[i] is JObject
|
||||
? ParsePosData(items[i])
|
||||
: items[i].ToString());
|
||||
}
|
||||
|
||||
result.TryAdd(item.Key, arrayResult);
|
||||
|
||||
break;
|
||||
case JTokenType.Object:
|
||||
result.TryAdd(item.Key, ParsePosData(item.Value));
|
||||
break;
|
||||
case null:
|
||||
break;
|
||||
default:
|
||||
result.TryAdd(item.Key, item.Value.ToString());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,14 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace BTCPayApp.CommonServer;
|
||||
|
||||
public class Roles
|
||||
{
|
||||
public const string ServerAdmin = "ServerAdmin";
|
||||
public static bool HasServerAdmin(IList<string> roles)
|
||||
{
|
||||
return roles.Contains(Roles.ServerAdmin, StringComparer.Ordinal);
|
||||
}
|
||||
}
|
@@ -36,7 +36,6 @@
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\BTCPayApp.CommonServer\BTCPayApp.CommonServer.csproj" />
|
||||
<ProjectReference Include="..\BTCPayServer.Client\BTCPayServer.Client.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
@@ -1,12 +1,11 @@
|
||||
namespace BTCPayApp.CommonServer
|
||||
namespace BTCPayServer.Abstractions.Constants
|
||||
{
|
||||
public class AuthenticationSchemes
|
||||
{
|
||||
public const string Cookie = "Identity.Application";
|
||||
public const string Bitpay = "Bitpay";
|
||||
public const string Greenfield = "Greenfield.APIKeys,Greenfield.Basic,Greenfield.Bearer";
|
||||
public const string Greenfield = "Greenfield.APIKeys,Greenfield.Basic";
|
||||
public const string GreenfieldAPIKeys = "Greenfield.APIKeys";
|
||||
public const string GreenfieldBasic = "Greenfield.Basic";
|
||||
public const string GreenfieldBearer = "Greenfield.Bearer";
|
||||
}
|
||||
}
|
@@ -1,4 +1,3 @@
|
||||
using BTCPayApp.CommonServer;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
|
@@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace BTCPayApp.CommonServer
|
||||
namespace BTCPayServer.Security
|
||||
{
|
||||
public class PolicyRequirement : IAuthorizationRequirement
|
||||
{
|
@@ -12,7 +12,7 @@
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<RepositoryUrl>https://github.com/btcpayserver/btcpayserver</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<Configurations>Debug;Release;Altcoins-Debug;Altcoins-Release</Configurations>
|
||||
<Configurations>Debug;Release</Configurations>
|
||||
<Platforms>AnyCPU</Platforms>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
|
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -58,6 +59,20 @@ public partial class BTCPayServerClient
|
||||
return await SendHttpRequest<CrowdfundAppData>($"api/v1/apps/crowdfund/{appId}", null, HttpMethod.Get, token);
|
||||
}
|
||||
|
||||
public virtual async Task<AppSalesStats> GetAppSales(string appId, int numberOfDays = 7, CancellationToken token = default)
|
||||
{
|
||||
if (appId == null) throw new ArgumentNullException(nameof(appId));
|
||||
var queryPayload = new Dictionary<string, object> { { nameof(numberOfDays), numberOfDays } };
|
||||
return await SendHttpRequest<AppSalesStats>($"api/v1/apps/{appId}/sales", queryPayload, HttpMethod.Get, token);
|
||||
}
|
||||
|
||||
public virtual async Task<List<AppItemStats>> GetAppTopItems(string appId, int offset = 0, int count = 10, CancellationToken token = default)
|
||||
{
|
||||
if (appId == null) throw new ArgumentNullException(nameof(appId));
|
||||
var queryPayload = new Dictionary<string, object> { { nameof(offset), offset }, { nameof(count), count } };
|
||||
return await SendHttpRequest<List<AppItemStats>>($"api/v1/apps/{appId}/top-items", queryPayload, HttpMethod.Get, token);
|
||||
}
|
||||
|
||||
public virtual async Task DeleteApp(string appId, CancellationToken token = default)
|
||||
{
|
||||
if (appId == null) throw new ArgumentNullException(nameof(appId));
|
||||
|
@@ -21,13 +21,6 @@ public partial class BTCPayServerClient
|
||||
return await SendHttpRequest<LightningNodeBalanceData>($"api/v1/server/lightning/{cryptoCode}/balance", null, HttpMethod.Get, token);
|
||||
}
|
||||
|
||||
public virtual async Task<HistogramData> GetLightningNodeHistogram(string cryptoCode, HistogramType? type = null,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
var queryPayload = type == null ? null : new Dictionary<string, string> { ["type"] = type.ToString() };
|
||||
return await SendHttpRequest<HistogramData>($"api/v1/server/lightning/{cryptoCode}/histogram", queryPayload, HttpMethod.Get, token);
|
||||
}
|
||||
|
||||
public virtual async Task ConnectToLightningNode(string cryptoCode, ConnectToNodeRequest request,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
|
@@ -21,13 +21,6 @@ public partial class BTCPayServerClient
|
||||
return await SendHttpRequest<LightningNodeBalanceData>($"api/v1/stores/{storeId}/lightning/{cryptoCode}/balance", null, HttpMethod.Get, token);
|
||||
}
|
||||
|
||||
public virtual async Task<HistogramData> GetLightningNodeHistogram(string storeId, string cryptoCode, HistogramType? type = null,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
var queryPayload = type == null ? null : new Dictionary<string, string> { ["type"] = type.ToString() };
|
||||
return await SendHttpRequest<HistogramData>($"api/v1/stores/{storeId}/lightning/{cryptoCode}/histogram", queryPayload, HttpMethod.Get, token);
|
||||
}
|
||||
|
||||
public virtual async Task ConnectToLightningNode(string storeId, string cryptoCode, ConnectToNodeRequest request,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
|
@@ -15,7 +15,7 @@ public partial class BTCPayServerClient
|
||||
int amount = 10,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
return await SendHttpRequest<UpdatePaymentMethodRequest, OnChainPaymentMethodPreviewResultData>($"api/v1/stores/{storeId}/payment-methods/{paymentMethodId}/preview",
|
||||
return await SendHttpRequest<UpdatePaymentMethodRequest, OnChainPaymentMethodPreviewResultData>($"api/v1/stores/{storeId}/payment-methods/{paymentMethodId}/wallet/preview",
|
||||
new Dictionary<string, object> { { "offset", offset }, { "amount", amount } },
|
||||
new UpdatePaymentMethodRequest { Config = JValue.CreateString(derivationScheme) },
|
||||
HttpMethod.Post, token);
|
||||
@@ -25,7 +25,7 @@ public partial class BTCPayServerClient
|
||||
string storeId, string paymentMethodId, int offset = 0, int amount = 10,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
return await SendHttpRequest<OnChainPaymentMethodPreviewResultData>($"api/v1/stores/{storeId}/payment-methods/{paymentMethodId}/preview",
|
||||
return await SendHttpRequest<OnChainPaymentMethodPreviewResultData>($"api/v1/stores/{storeId}/payment-methods/{paymentMethodId}/wallet/preview",
|
||||
new Dictionary<string, object> { { "offset", offset }, { "amount", amount } }, HttpMethod.Get, token);
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ public partial class BTCPayServerClient
|
||||
string paymentMethodId, GenerateOnChainWalletRequest request,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
return await SendHttpRequest<GenerateOnChainWalletResponse>($"api/v1/stores/{storeId}/payment-methods/{paymentMethodId}/generate", request, HttpMethod.Post, token);
|
||||
return await SendHttpRequest<GenerateOnChainWalletResponse>($"api/v1/stores/{storeId}/payment-methods/{paymentMethodId}/wallet/generate", request, HttpMethod.Post, token);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -15,7 +15,7 @@ public partial class BTCPayServerClient
|
||||
parameters.Add("includeNeighbourData", v);
|
||||
try
|
||||
{
|
||||
return await SendHttpRequest<OnChainWalletObjectData>($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/objects/{objectId.Type}/{objectId.Id}", parameters, HttpMethod.Get, token);
|
||||
return await SendHttpRequest<OnChainWalletObjectData>($"api/v1/stores/{storeId}/payment-methods/{cryptoCode}-CHAIN/wallet/objects/{objectId.Type}/{objectId.Id}", parameters, HttpMethod.Get, token);
|
||||
}
|
||||
catch (GreenfieldAPIException err) when (err.APIError.Code == "wallet-object-not-found")
|
||||
{
|
||||
@@ -31,17 +31,17 @@ public partial class BTCPayServerClient
|
||||
parameters.Add("ids", ids);
|
||||
if (query?.IncludeNeighbourData is bool v)
|
||||
parameters.Add("includeNeighbourData", v);
|
||||
return await SendHttpRequest<OnChainWalletObjectData[]>($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/objects", parameters, HttpMethod.Get, token);
|
||||
return await SendHttpRequest<OnChainWalletObjectData[]>($"api/v1/stores/{storeId}/payment-methods/{cryptoCode}-CHAIN/wallet/objects", parameters, HttpMethod.Get, token);
|
||||
}
|
||||
public virtual async Task RemoveOnChainWalletObject(string storeId, string cryptoCode, OnChainWalletObjectId objectId,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
await SendHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/objects/{objectId.Type}/{objectId.Id}", null, HttpMethod.Delete, token);
|
||||
await SendHttpRequest($"api/v1/stores/{storeId}/payment-methods/{cryptoCode}-CHAIN/wallet/objects/{objectId.Type}/{objectId.Id}", null, HttpMethod.Delete, token);
|
||||
}
|
||||
public virtual async Task<OnChainWalletObjectData> AddOrUpdateOnChainWalletObject(string storeId, string cryptoCode, AddOnChainWalletObjectRequest request,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
return await SendHttpRequest<OnChainWalletObjectData>($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/objects", request, HttpMethod.Post, token);
|
||||
return await SendHttpRequest<OnChainWalletObjectData>($"api/v1/stores/{storeId}/payment-methods/{cryptoCode}-CHAIN/wallet/objects", request, HttpMethod.Post, token);
|
||||
}
|
||||
|
||||
public virtual async Task AddOrUpdateOnChainWalletLink(string storeId, string cryptoCode,
|
||||
@@ -49,7 +49,7 @@ public partial class BTCPayServerClient
|
||||
AddOnChainWalletObjectLinkRequest request = null,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
await SendHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/objects/{objectId.Type}/{objectId.Id}/links", request, HttpMethod.Post, token);
|
||||
await SendHttpRequest($"api/v1/stores/{storeId}/payment-methods/{cryptoCode}-CHAIN/wallet/objects/{objectId.Type}/{objectId.Id}/links", request, HttpMethod.Post, token);
|
||||
}
|
||||
|
||||
public virtual async Task RemoveOnChainWalletLinks(string storeId, string cryptoCode,
|
||||
@@ -57,6 +57,6 @@ public partial class BTCPayServerClient
|
||||
OnChainWalletObjectId link,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
await SendHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/objects/{objectId.Type}/{objectId.Id}/links/{link.Type}/{link.Id}", null, HttpMethod.Delete, token);
|
||||
await SendHttpRequest($"api/v1/stores/{storeId}/payment-methods/{cryptoCode}-CHAIN/wallet/objects/{objectId.Type}/{objectId.Id}/links/{link.Type}/{link.Id}", null, HttpMethod.Delete, token);
|
||||
}
|
||||
}
|
||||
|
@@ -14,16 +14,8 @@ public partial class BTCPayServerClient
|
||||
public virtual async Task<OnChainWalletOverviewData> ShowOnChainWalletOverview(string storeId, string cryptoCode,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
return await SendHttpRequest<OnChainWalletOverviewData>($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet", null, HttpMethod.Get, token);
|
||||
return await SendHttpRequest<OnChainWalletOverviewData>($"api/v1/stores/{storeId}/payment-methods/{cryptoCode}-CHAIN/wallet", null, HttpMethod.Get, token);
|
||||
}
|
||||
|
||||
public virtual async Task<HistogramData> GetOnChainWalletHistogram(string storeId, string cryptoCode, HistogramType? type = null,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
var queryPayload = type == null ? null : new Dictionary<string, string> { ["type"] = type.ToString() };
|
||||
return await SendHttpRequest<HistogramData>($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/histogram", queryPayload, HttpMethod.Get, token);
|
||||
}
|
||||
|
||||
public virtual async Task<OnChainWalletFeeRateData> GetOnChainFeeRate(string storeId, string cryptoCode, int? blockTarget = null,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
@@ -32,13 +24,13 @@ public partial class BTCPayServerClient
|
||||
{
|
||||
queryParams.Add("blockTarget", blockTarget);
|
||||
}
|
||||
return await SendHttpRequest<OnChainWalletFeeRateData>($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/feeRate", queryParams, HttpMethod.Get, token);
|
||||
return await SendHttpRequest<OnChainWalletFeeRateData>($"api/v1/stores/{storeId}/payment-methods/{cryptoCode}-CHAIN/wallet/feerate", queryParams, HttpMethod.Get, token);
|
||||
}
|
||||
|
||||
public virtual async Task<OnChainWalletAddressData> GetOnChainWalletReceiveAddress(string storeId, string cryptoCode, bool forceGenerate = false,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
return await SendHttpRequest<OnChainWalletAddressData>($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/address", new Dictionary<string, object>
|
||||
return await SendHttpRequest<OnChainWalletAddressData>($"api/v1/stores/{storeId}/payment-methods/{cryptoCode}-CHAIN/wallet/address", new Dictionary<string, object>
|
||||
{
|
||||
{"forceGenerate", forceGenerate}
|
||||
}, HttpMethod.Get, token);
|
||||
@@ -47,7 +39,7 @@ public partial class BTCPayServerClient
|
||||
public virtual async Task UnReserveOnChainWalletReceiveAddress(string storeId, string cryptoCode,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
await SendHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/address", null, HttpMethod.Delete, token);
|
||||
await SendHttpRequest($"api/v1/stores/{storeId}/payment-methods/{cryptoCode}-CHAIN/wallet/address", null, HttpMethod.Delete, token);
|
||||
}
|
||||
|
||||
public virtual async Task<IEnumerable<OnChainWalletTransactionData>> ShowOnChainWalletTransactions(
|
||||
@@ -67,14 +59,14 @@ public partial class BTCPayServerClient
|
||||
{
|
||||
query.Add(nameof(skip), skip);
|
||||
}
|
||||
return await SendHttpRequest<IEnumerable<OnChainWalletTransactionData>>($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/transactions", query, HttpMethod.Get, token);
|
||||
return await SendHttpRequest<IEnumerable<OnChainWalletTransactionData>>($"api/v1/stores/{storeId}/payment-methods/{cryptoCode}-CHAIN/wallet/transactions", query, HttpMethod.Get, token);
|
||||
}
|
||||
|
||||
public virtual async Task<OnChainWalletTransactionData> GetOnChainWalletTransaction(
|
||||
string storeId, string cryptoCode, string transactionId,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
return await SendHttpRequest<OnChainWalletTransactionData>($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/transactions/{transactionId}", null, HttpMethod.Get, token);
|
||||
return await SendHttpRequest<OnChainWalletTransactionData>($"api/v1/stores/{storeId}/payment-methods/{cryptoCode}-CHAIN/wallet/transactions/{transactionId}", null, HttpMethod.Get, token);
|
||||
}
|
||||
|
||||
public virtual async Task<OnChainWalletTransactionData> PatchOnChainWalletTransaction(
|
||||
@@ -82,7 +74,7 @@ public partial class BTCPayServerClient
|
||||
PatchOnChainTransactionRequest request,
|
||||
bool force = false, CancellationToken token = default)
|
||||
{
|
||||
return await SendHttpRequest<PatchOnChainTransactionRequest, OnChainWalletTransactionData>($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/transactions/{transactionId}",
|
||||
return await SendHttpRequest<PatchOnChainTransactionRequest, OnChainWalletTransactionData>($"api/v1/stores/{storeId}/payment-methods/{cryptoCode}-CHAIN/wallet/transactions/{transactionId}",
|
||||
new Dictionary<string, object> { {"force", force} }, request, HttpMethod.Patch, token);
|
||||
}
|
||||
|
||||
@@ -90,7 +82,7 @@ public partial class BTCPayServerClient
|
||||
string cryptoCode,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
return await SendHttpRequest<IEnumerable<OnChainWalletUTXOData>>($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/utxos", null, HttpMethod.Get, token);
|
||||
return await SendHttpRequest<IEnumerable<OnChainWalletUTXOData>>($"api/v1/stores/{storeId}/payment-methods/{cryptoCode}-CHAIN/wallet/utxos", null, HttpMethod.Get, token);
|
||||
}
|
||||
|
||||
public virtual async Task<OnChainWalletTransactionData> CreateOnChainTransaction(string storeId,
|
||||
@@ -102,7 +94,7 @@ public partial class BTCPayServerClient
|
||||
throw new ArgumentOutOfRangeException(nameof(request.ProceedWithBroadcast),
|
||||
"Please use CreateOnChainTransactionButDoNotBroadcast when wanting to only create the transaction");
|
||||
}
|
||||
return await SendHttpRequest<OnChainWalletTransactionData>($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/transactions", request, HttpMethod.Post, token);
|
||||
return await SendHttpRequest<OnChainWalletTransactionData>($"api/v1/stores/{storeId}/payment-methods/{cryptoCode}-CHAIN/wallet/transactions", request, HttpMethod.Post, token);
|
||||
}
|
||||
|
||||
public virtual async Task<Transaction> CreateOnChainTransactionButDoNotBroadcast(string storeId,
|
||||
@@ -114,6 +106,6 @@ public partial class BTCPayServerClient
|
||||
throw new ArgumentOutOfRangeException(nameof(request.ProceedWithBroadcast),
|
||||
"Please use CreateOnChainTransaction when wanting to also broadcast the transaction");
|
||||
}
|
||||
return Transaction.Parse(await SendHttpRequest<string>($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/transactions", request, HttpMethod.Post, token), network);
|
||||
return Transaction.Parse(await SendHttpRequest<string>($"api/v1/stores/{storeId}/payment-methods/{cryptoCode}-CHAIN/wallet/transactions", request, HttpMethod.Post, token), network);
|
||||
}
|
||||
}
|
||||
|
15
BTCPayServer.Client/Models/AppItemStats.cs
Normal file
15
BTCPayServer.Client/Models/AppItemStats.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using BTCPayServer.JsonConverters;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Client.Models;
|
||||
|
||||
public class AppItemStats
|
||||
{
|
||||
public string ItemCode { get; set; }
|
||||
public string Title { get; set; }
|
||||
public int SalesCount { get; set; }
|
||||
|
||||
[JsonConverter(typeof(NumericStringJsonConverter))]
|
||||
public decimal Total { get; set; }
|
||||
public string TotalFormatted { get; set; }
|
||||
}
|
19
BTCPayServer.Client/Models/AppSalesStats.cs
Normal file
19
BTCPayServer.Client/Models/AppSalesStats.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Client.Models;
|
||||
|
||||
public class AppSalesStats
|
||||
{
|
||||
public int SalesCount { get; set; }
|
||||
public IEnumerable<AppSalesStatsItem> Series { get; set; }
|
||||
}
|
||||
|
||||
public class AppSalesStatsItem
|
||||
{
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
|
||||
public DateTime Date { get; set; }
|
||||
public string Label { get; set; }
|
||||
public int SalesCount { get; set; }
|
||||
}
|
@@ -8,6 +8,6 @@ namespace BTCPayServer.Client.Models
|
||||
public string Destination { get; set; }
|
||||
[JsonConverter(typeof(NumericStringJsonConverter))]
|
||||
public decimal? Amount { get; set; }
|
||||
public string PaymentMethod { get; set; }
|
||||
public string PayoutMethodId { get; set; }
|
||||
}
|
||||
}
|
||||
|
@@ -10,6 +10,7 @@ namespace BTCPayServer.Client
|
||||
{
|
||||
public class GenerateOnChainWalletRequest
|
||||
{
|
||||
public string Label { get; set; }
|
||||
public int AccountNumber { get; set; } = 0;
|
||||
[JsonConverter(typeof(MnemonicJsonConverter))]
|
||||
public Mnemonic ExistingMnemonic { get; set; }
|
||||
@@ -29,6 +30,7 @@ namespace BTCPayServer.Client
|
||||
{
|
||||
public class ConfigData
|
||||
{
|
||||
public string Label { get; set; }
|
||||
public string AccountDerivation { get; set; }
|
||||
[JsonExtensionData]
|
||||
IDictionary<string, JToken> AdditionalData { get; set; }
|
||||
|
@@ -6,7 +6,7 @@ namespace BTCPayServer.Client.Models;
|
||||
|
||||
public class LightningAutomatedPayoutSettings
|
||||
{
|
||||
public string PaymentMethod { get; set; }
|
||||
public string PayoutMethodId { get; set; }
|
||||
|
||||
[JsonConverter(typeof(TimeSpanJsonConverter.Seconds))]
|
||||
public TimeSpan IntervalSeconds { get; set; }
|
||||
|
@@ -6,7 +6,7 @@ namespace BTCPayServer.Client.Models;
|
||||
|
||||
public class OnChainAutomatedPayoutSettings
|
||||
{
|
||||
public string PaymentMethod { get; set; }
|
||||
public string PayoutMethodId { get; set; }
|
||||
|
||||
[JsonConverter(typeof(TimeSpanJsonConverter.Seconds))]
|
||||
public TimeSpan IntervalSeconds { get; set; }
|
||||
|
@@ -5,7 +5,7 @@ namespace BTCPayServer.Client.Models;
|
||||
|
||||
public class PaymentMethodCriteriaData
|
||||
{
|
||||
public string PaymentMethod { get; set; }
|
||||
public string PaymentMethodId { get; set; }
|
||||
public string CurrencyCode { get; set; }
|
||||
[JsonConverter(typeof(NumericStringJsonConverter))]
|
||||
public decimal Amount { get; set; }
|
||||
|
@@ -21,7 +21,7 @@ namespace BTCPayServer.Client.Models
|
||||
public string Id { get; set; }
|
||||
public string PullPaymentId { get; set; }
|
||||
public string Destination { get; set; }
|
||||
public string PaymentMethod { get; set; }
|
||||
public string PayoutMethodId { get; set; }
|
||||
public string CryptoCode { get; set; }
|
||||
[JsonConverter(typeof(NumericStringJsonConverter))]
|
||||
public decimal Amount { get; set; }
|
||||
|
@@ -17,7 +17,7 @@ namespace BTCPayServer.Client.Models
|
||||
public class RefundInvoiceRequest
|
||||
{
|
||||
public string? Name { get; set; } = null;
|
||||
public string? PaymentMethod { get; set; }
|
||||
public string? PayoutMethodId { get; set; }
|
||||
public string? Description { get; set; } = null;
|
||||
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
|
@@ -17,6 +17,7 @@ namespace BTCPayServer.Client.Models
|
||||
public string Website { get; set; }
|
||||
|
||||
public string BrandColor { get; set; }
|
||||
public bool ApplyBrandColorToBackend { get; set; }
|
||||
public string LogoUrl { get; set; }
|
||||
public string CssUrl { get; set; }
|
||||
public string PaymentSoundUrl { get; set; }
|
||||
|
@@ -1,24 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.JsonConverters;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
|
||||
namespace BTCPayServer.Client.Models;
|
||||
|
||||
public enum HistogramType
|
||||
{
|
||||
Week,
|
||||
Month,
|
||||
Year
|
||||
}
|
||||
|
||||
public class HistogramData
|
||||
{
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public HistogramType Type { get; set; }
|
||||
public List<decimal> Series { get; set; }
|
||||
public List<DateTimeOffset> Labels { get; set; }
|
||||
[JsonConverter(typeof(NumericStringJsonConverter))]
|
||||
public decimal Balance { get; set; }
|
||||
}
|
@@ -1,8 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace BTCPayServer.Client
|
||||
{
|
||||
@@ -16,7 +18,7 @@ namespace BTCPayServer.Client
|
||||
public const string CanUseLightningNodeInStore = "btcpay.store.canuselightningnode";
|
||||
public const string CanModifyServerSettings = "btcpay.server.canmodifyserversettings";
|
||||
public const string CanModifyStoreSettings = "btcpay.store.canmodifystoresettings";
|
||||
public const string CanModifyStoreWebhooks = "btcpay.store.webhooks.canmodifywebhooks";
|
||||
public const string CanModifyWebhooks = "btcpay.store.webhooks.canmodifywebhooks";
|
||||
public const string CanModifyStoreSettingsUnscoped = "btcpay.store.canmodifystoresettings:";
|
||||
public const string CanViewStoreSettings = "btcpay.store.canviewstoresettings";
|
||||
public const string CanViewReports = "btcpay.store.canviewreports";
|
||||
@@ -48,7 +50,7 @@ namespace BTCPayServer.Client
|
||||
yield return CanViewInvoices;
|
||||
yield return CanCreateInvoice;
|
||||
yield return CanModifyInvoices;
|
||||
yield return CanModifyStoreWebhooks;
|
||||
yield return CanModifyWebhooks;
|
||||
yield return CanModifyServerSettings;
|
||||
yield return CanModifyStoreSettings;
|
||||
yield return CanViewStoreSettings;
|
||||
@@ -104,6 +106,16 @@ namespace BTCPayServer.Client
|
||||
{
|
||||
return policy.StartsWith("btcpay.user", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static readonly CultureInfo _culture = new (CultureInfo.InvariantCulture.Name);
|
||||
public static string DisplayName(string policy)
|
||||
{
|
||||
var p = policy.Split(".");
|
||||
if (p.Length < 3 || p[0] != "btcpay") return policy;
|
||||
var constName = typeof(Policies).GetFields().Select(f => f.Name).FirstOrDefault(f => f.Equals(p[^1], StringComparison.OrdinalIgnoreCase));
|
||||
var perm = string.IsNullOrEmpty(constName) ? string.Join(' ', p[2..]) : Regex.Replace(constName, "([A-Z])", " $1", RegexOptions.Compiled).Trim();
|
||||
return $"{_culture.TextInfo.ToTitleCase(p[1])}: {_culture.TextInfo.ToTitleCase(perm)}";
|
||||
}
|
||||
}
|
||||
|
||||
public class PermissionSet
|
||||
@@ -247,7 +259,7 @@ namespace BTCPayServer.Client
|
||||
Policies.CanManagePullPayments,
|
||||
Policies.CanModifyInvoices,
|
||||
Policies.CanViewStoreSettings,
|
||||
Policies.CanModifyStoreWebhooks,
|
||||
Policies.CanModifyWebhooks,
|
||||
Policies.CanModifyPaymentRequests,
|
||||
Policies.CanManagePayouts,
|
||||
Policies.CanUseLightningNodeInStore);
|
||||
|
@@ -4,6 +4,8 @@ using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using BTCPayServer.Common;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using NBitcoin;
|
||||
using NBXplorer;
|
||||
using NBXplorer.Models;
|
||||
@@ -131,7 +133,7 @@ namespace BTCPayServer
|
||||
|
||||
public string GetTrackedDestination(Script scriptPubKey)
|
||||
{
|
||||
return scriptPubKey.Hash.ToString() + "#" + CryptoCode.ToUpperInvariant();
|
||||
return scriptPubKey.Hash.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -3,9 +3,13 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Logging;
|
||||
using Microsoft.AspNetCore.DataProtection.KeyManagement;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin;
|
||||
using NBXplorer;
|
||||
using StandardConfiguration;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
@@ -30,11 +34,6 @@ namespace BTCPayServer
|
||||
Logs logs)
|
||||
{
|
||||
var networksList = networks.ToList();
|
||||
#if !ALTCOINS
|
||||
var onlyBTC = networksList.Count == 1 && networksList.First().IsBTC;
|
||||
if (!onlyBTC)
|
||||
throw new ConfigException($"This build of BTCPay Server does not support altcoins. Configured networks: {string.Join(',', networksList.Select(n => n.CryptoCode).ToArray())}");
|
||||
#endif
|
||||
_NBXplorerNetworkProvider = nbxplorerNetworkProvider;
|
||||
NetworkType = nbxplorerNetworkProvider.NetworkType;
|
||||
foreach (var network in networksList)
|
||||
|
@@ -1,13 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="../Build/Version.csproj" Condition="Exists('../Build/Version.csproj')" />
|
||||
<Import Project="../Build/Common.csproj" />
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="4.3.1" />
|
||||
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="2.0.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(Altcoins)' != 'true'">
|
||||
<Compile Remove="Altcoins\**\*.cs"></Compile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="Altcoins\" />
|
||||
</ItemGroup>
|
||||
|
@@ -1,5 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
|
@@ -4,6 +4,7 @@ using System.Linq;
|
||||
using BTCPayServer.Logging;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json.Bson;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
@@ -11,17 +12,12 @@ namespace BTCPayServer
|
||||
{
|
||||
HashSet<string> chains = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
bool all = false;
|
||||
public SelectedChains(IConfiguration configuration, Logs logs)
|
||||
public SelectedChains(IConfiguration configuration)
|
||||
{
|
||||
foreach (var chain in (configuration["chains"] ?? "btc")
|
||||
.Split(',', StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(t => t.ToUpperInvariant()))
|
||||
{
|
||||
if (new[] { "ETH", "USDT20", "FAU" }.Contains(chain, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
logs.Configuration.LogWarning($"'{chain}' is not anymore supported, please remove it from 'chains'");
|
||||
continue;
|
||||
}
|
||||
if (chain == "*")
|
||||
{
|
||||
all = true;
|
||||
|
@@ -1,32 +0,0 @@
|
||||
using BTCPayServer.Data;
|
||||
using Laraue.EfCoreTriggers.Common.Extensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace BTCPayServer.App.BackupStorage;
|
||||
|
||||
public class AppStorageItemData
|
||||
{
|
||||
public string Key { get; set; }
|
||||
public long Version { get; set; }
|
||||
public byte[] Value { get; set; }
|
||||
public string UserId { get; set; }
|
||||
|
||||
public static void OnModelCreating(ModelBuilder builder)
|
||||
{
|
||||
builder.Entity<AppStorageItemData>()
|
||||
.HasKey(o => new {o.Key, o.Version, o.UserId});
|
||||
builder.Entity<AppStorageItemData>()
|
||||
.HasIndex(o => new {o.Key, o.UserId}).IsUnique();
|
||||
|
||||
builder.Entity<ApplicationUser>().HasMany(user => user.AppStorageItems).WithOne(data => data.User)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.Entity<AppStorageItemData>()
|
||||
.BeforeInsert(trigger => trigger
|
||||
.Action(group => group
|
||||
.Delete<AppStorageItemData>((@ref, entity) => @ref.New.UserId == entity.UserId && @ref.New.Key == entity.Key &&
|
||||
@ref.New.Version > entity.Version)));
|
||||
}
|
||||
|
||||
public ApplicationUser User { get; set; }
|
||||
}
|
@@ -1,8 +1,6 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.App.BackupStorage;
|
||||
using Laraue.EfCoreTriggers.PostgreSql.Extensions;
|
||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Design;
|
||||
@@ -17,7 +15,6 @@ namespace BTCPayServer.Data
|
||||
var builder = new DbContextOptionsBuilder<ApplicationDbContext>();
|
||||
// Same as launchsettings.json, it's connecting to the docker's postgres.
|
||||
builder.UseNpgsql("User ID=postgres;Include Error Detail=true;Host=127.0.0.1;Port=39372;Database=btcpayserver");
|
||||
builder.UsePostgreSqlTriggers();
|
||||
return new ApplicationDbContext(builder.Options);
|
||||
}
|
||||
}
|
||||
@@ -28,8 +25,6 @@ namespace BTCPayServer.Data
|
||||
{
|
||||
}
|
||||
public DbSet<AddressInvoiceData> AddressInvoices { get; set; }
|
||||
public DbSet<AppStorageItemData> AppStorageItems { get; set; }
|
||||
|
||||
public DbSet<APIKeyData> ApiKeys { get; set; }
|
||||
public DbSet<AppData> Apps { get; set; }
|
||||
public DbSet<StoredFile> Files { get; set; }
|
||||
@@ -44,7 +39,6 @@ namespace BTCPayServer.Data
|
||||
public DbSet<PaymentRequestData> PaymentRequests { get; set; }
|
||||
public DbSet<PaymentData> Payments { get; set; }
|
||||
public DbSet<PayoutData> Payouts { get; set; }
|
||||
public DbSet<PendingInvoiceData> PendingInvoices { get; set; }
|
||||
public DbSet<PlannedTransaction> PlannedTransactions { get; set; }
|
||||
public DbSet<PullPaymentData> PullPayments { get; set; }
|
||||
public DbSet<RefundData> Refunds { get; set; }
|
||||
@@ -73,7 +67,7 @@ namespace BTCPayServer.Data
|
||||
base.OnModelCreating(builder);
|
||||
|
||||
// some of the data models don't have OnModelCreating for now, commenting them
|
||||
AppStorageItemData.OnModelCreating(builder);
|
||||
|
||||
ApplicationUser.OnModelCreating(builder, Database);
|
||||
AddressInvoiceData.OnModelCreating(builder);
|
||||
APIKeyData.OnModelCreating(builder, Database);
|
||||
@@ -88,9 +82,8 @@ namespace BTCPayServer.Data
|
||||
PairingCodeData.OnModelCreating(builder);
|
||||
//PayjoinLock.OnModelCreating(builder);
|
||||
PaymentRequestData.OnModelCreating(builder, Database);
|
||||
PaymentData.OnModelCreating(builder, Database);
|
||||
PaymentData.OnModelCreating(builder);
|
||||
PayoutData.OnModelCreating(builder, Database);
|
||||
PendingInvoiceData.OnModelCreating(builder);
|
||||
//PlannedTransaction.OnModelCreating(builder);
|
||||
PullPaymentData.OnModelCreating(builder, Database);
|
||||
RefundData.OnModelCreating(builder);
|
||||
|
@@ -1,10 +1,8 @@
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using Laraue.EfCoreTriggers.PostgreSql.Extensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure;
|
||||
|
||||
@@ -12,20 +10,20 @@ namespace BTCPayServer.Data
|
||||
{
|
||||
public class ApplicationDbContextFactory : BaseDbContextFactory<ApplicationDbContext>
|
||||
{
|
||||
public ApplicationDbContextFactory(IOptions<DatabaseOptions> options) : base(options, "")
|
||||
public ApplicationDbContextFactory(IOptions<DatabaseOptions> options, ILoggerFactory loggerFactory) : base(options, "")
|
||||
{
|
||||
LoggerFactory = loggerFactory;
|
||||
}
|
||||
|
||||
public ILoggerFactory LoggerFactory { get; }
|
||||
|
||||
public override ApplicationDbContext CreateContext(Action<NpgsqlDbContextOptionsBuilder> npgsqlOptionsAction = null)
|
||||
{
|
||||
var builder = new DbContextOptionsBuilder<ApplicationDbContext>();
|
||||
builder.AddInterceptors(Data.InvoiceData.MigrationInterceptor.Instance);
|
||||
builder.UseLoggerFactory(LoggerFactory);
|
||||
builder.AddInterceptors(MigrationInterceptor.Instance);
|
||||
ConfigureBuilder(builder, npgsqlOptionsAction);
|
||||
|
||||
builder.UsePostgreSqlTriggers();
|
||||
return new ApplicationDbContext(builder.Options);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -3,7 +3,6 @@
|
||||
<Import Project="../Build/Common.csproj" />
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
<PackageReference Include="Laraue.EfCoreTriggers.PostgreSql" Version="8.0.3" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.6">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
@@ -18,4 +17,10 @@
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="DBScripts\*.sql" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Remove="DBScripts\001.InvoiceFunctions.sql" />
|
||||
<None Remove="DBScripts\002.RefactorPayouts.sql" />
|
||||
<None Remove="DBScripts\003.RefactorPendingInvoicesPayments.sql" />
|
||||
<None Remove="DBScripts\004.MonitoredInvoices.sql" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
12
BTCPayServer.Data/DBScripts/001.InvoiceFunctions.sql
Normal file
12
BTCPayServer.Data/DBScripts/001.InvoiceFunctions.sql
Normal file
@@ -0,0 +1,12 @@
|
||||
CREATE OR REPLACE FUNCTION get_orderid(invoice_blob jsonb)
|
||||
RETURNS text AS $$
|
||||
SELECT invoice_blob->'metadata'->>'orderId';
|
||||
$$ LANGUAGE sql IMMUTABLE;
|
||||
|
||||
CREATE OR REPLACE FUNCTION get_itemcode(invoice_blob jsonb)
|
||||
RETURNS text AS $$
|
||||
SELECT invoice_blob->'metadata'->>'itemCode';
|
||||
$$ LANGUAGE sql IMMUTABLE;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "IX_Invoices_Metadata_OrderId" ON "Invoices" (get_orderid("Blob2")) WHERE get_orderid("Blob2") IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS "IX_Invoices_Metadata_ItemCode" ON "Invoices" (get_itemcode("Blob2")) WHERE get_itemcode("Blob2") IS NOT NULL;
|
62
BTCPayServer.Data/DBScripts/002.RefactorPayouts.sql
Normal file
62
BTCPayServer.Data/DBScripts/002.RefactorPayouts.sql
Normal file
@@ -0,0 +1,62 @@
|
||||
-- Rename column
|
||||
ALTER TABLE "Payouts" RENAME COLUMN "PaymentMethodId" TO "PayoutMethodId";
|
||||
|
||||
-- Add Currency column, guessed from the PaymentMethodId
|
||||
ALTER TABLE "Payouts" ADD COLUMN "Currency" TEXT;
|
||||
UPDATE "Payouts" SET
|
||||
"Currency" = split_part("PayoutMethodId", '_', 1),
|
||||
"PayoutMethodId"=
|
||||
CASE
|
||||
WHEN ("Blob"->>'Amount')::NUMERIC < 0 THEN 'TOPUP'
|
||||
WHEN split_part("PayoutMethodId", '_', 2) = 'LightningLike' THEN split_part("PayoutMethodId", '_', 1) || '-LN'
|
||||
ELSE split_part("PayoutMethodId", '_', 1) || '-CHAIN'
|
||||
END;
|
||||
ALTER TABLE "Payouts" ALTER COLUMN "Currency" SET NOT NULL;
|
||||
|
||||
-- Remove Currency and Limit from PullPayment Blob, and put it into the columns in the table
|
||||
ALTER TABLE "PullPayments" ADD COLUMN "Currency" TEXT;
|
||||
UPDATE "PullPayments" SET "Currency" = "Blob"->>'Currency';
|
||||
ALTER TABLE "PullPayments" ALTER COLUMN "Currency" SET NOT NULL;
|
||||
ALTER TABLE "PullPayments" ADD COLUMN "Limit" NUMERIC;
|
||||
UPDATE "PullPayments" SET "Limit" = ("Blob"->>'Limit')::NUMERIC;
|
||||
ALTER TABLE "PullPayments" ALTER COLUMN "Limit" SET NOT NULL;
|
||||
|
||||
-- Remove unused properties, rename SupportedPaymentMethods, and fix legacy payment methods IDs
|
||||
UPDATE "PullPayments" SET
|
||||
"Blob" = jsonb_set(
|
||||
"Blob" - 'SupportedPaymentMethods' - 'Limit' - 'Currency' - 'Period',
|
||||
'{SupportedPayoutMethods}',
|
||||
(SELECT jsonb_agg(to_jsonb(
|
||||
CASE
|
||||
WHEN split_part(value::TEXT, '_', 2) = 'LightningLike' THEN split_part(value::TEXT, '_', 1) || '-LN'
|
||||
ELSE split_part(value::TEXT, '_', 1) || '-CHAIN'
|
||||
END))
|
||||
FROM jsonb_array_elements_text("Blob"->'SupportedPaymentMethods') AS value
|
||||
));
|
||||
|
||||
--Remove "Amount" and "CryptoAmount" from Payout Blob, and put it into the columns in the table
|
||||
-- Respectively "OriginalAmount" and "Amount"
|
||||
|
||||
ALTER TABLE "Payouts" ADD COLUMN "Amount" NUMERIC;
|
||||
UPDATE "Payouts" SET "Amount" = ("Blob"->>'CryptoAmount')::NUMERIC;
|
||||
|
||||
ALTER TABLE "Payouts" ADD COLUMN "OriginalAmount" NUMERIC;
|
||||
UPDATE "Payouts" SET "OriginalAmount" = ("Blob"->>'Amount')::NUMERIC;
|
||||
ALTER TABLE "Payouts" ALTER COLUMN "OriginalAmount" SET NOT NULL;
|
||||
|
||||
ALTER TABLE "Payouts" ADD COLUMN "OriginalCurrency" TEXT;
|
||||
|
||||
|
||||
UPDATE "Payouts" p
|
||||
SET
|
||||
"OriginalCurrency" = "Currency",
|
||||
"Blob" = "Blob" - 'Amount' - 'CryptoAmount'
|
||||
WHERE "PullPaymentDataId" IS NULL AND "OriginalCurrency" IS NULL;
|
||||
|
||||
UPDATE "Payouts" p
|
||||
SET
|
||||
"OriginalCurrency" = pp."Currency"
|
||||
FROM "PullPayments" pp
|
||||
WHERE "OriginalCurrency" IS NULL AND pp."Id"=p."PullPaymentDataId";
|
||||
|
||||
ALTER TABLE "Payouts" ALTER COLUMN "OriginalCurrency" SET NOT NULL;
|
@@ -0,0 +1,9 @@
|
||||
CREATE OR REPLACE FUNCTION is_pending(status TEXT)
|
||||
RETURNS BOOLEAN AS $$
|
||||
SELECT status = 'Processing' OR status = 'New';
|
||||
$$ LANGUAGE sql IMMUTABLE;
|
||||
|
||||
CREATE INDEX "IX_Invoices_Pending" ON "Invoices"((1)) WHERE is_pending("Status");
|
||||
CREATE INDEX "IX_Payments_Pending" ON "Payments"((1)) WHERE is_pending("Status");
|
||||
DROP TABLE "PendingInvoices";
|
||||
ANALYZE "Invoices";
|
23
BTCPayServer.Data/DBScripts/004.MonitoredInvoices.sql
Normal file
23
BTCPayServer.Data/DBScripts/004.MonitoredInvoices.sql
Normal file
@@ -0,0 +1,23 @@
|
||||
CREATE OR REPLACE FUNCTION get_prompt(invoice_blob JSONB, payment_method_id TEXT)
|
||||
RETURNS JSONB AS $$
|
||||
SELECT invoice_blob->'prompts'->payment_method_id
|
||||
$$ LANGUAGE sql IMMUTABLE;
|
||||
|
||||
|
||||
CREATE OR REPLACE FUNCTION get_monitored_invoices(arg_payment_method_id TEXT, include_non_activated BOOLEAN)
|
||||
RETURNS TABLE (invoice_id TEXT, payment_id TEXT, payment_method_id TEXT) AS $$
|
||||
WITH cte AS (
|
||||
-- Get all the invoices which are pending. Even if no payments.
|
||||
SELECT i."Id" invoice_id, p."Id" payment_id, p."PaymentMethodId" payment_method_id FROM "Invoices" i LEFT JOIN "Payments" p ON i."Id" = p."InvoiceDataId"
|
||||
WHERE is_pending(i."Status")
|
||||
UNION ALL
|
||||
-- For invoices not pending, take all of those which have pending payments
|
||||
SELECT i."Id" invoice_id, p."Id" payment_id, p."PaymentMethodId" payment_method_id FROM "Invoices" i INNER JOIN "Payments" p ON i."Id" = p."InvoiceDataId"
|
||||
WHERE is_pending(p."Status") AND NOT is_pending(i."Status"))
|
||||
SELECT cte.* FROM cte
|
||||
JOIN "Invoices" i ON cte.invoice_id=i."Id"
|
||||
LEFT JOIN "Payments" p ON cte.payment_id=p."Id" AND cte.payment_method_id=p."PaymentMethodId"
|
||||
WHERE (p."PaymentMethodId" IS NOT NULL AND p."PaymentMethodId" = arg_payment_method_id) OR
|
||||
(p."PaymentMethodId" IS NULL AND get_prompt(i."Blob2", arg_payment_method_id) IS NOT NULL AND
|
||||
(include_non_activated IS TRUE OR (get_prompt(i."Blob2", arg_payment_method_id)->'inactive')::BOOLEAN IS NOT TRUE));
|
||||
$$ LANGUAGE SQL STABLE;
|
@@ -9,6 +9,7 @@ namespace BTCPayServer.Data
|
||||
public string Address { get; set; }
|
||||
public InvoiceData InvoiceData { get; set; }
|
||||
public string InvoiceDataId { get; set; }
|
||||
public string PaymentMethodId { get; set; }
|
||||
|
||||
|
||||
internal static void OnModelCreating(ModelBuilder builder)
|
||||
@@ -18,7 +19,7 @@ namespace BTCPayServer.Data
|
||||
.WithMany(i => i.AddressInvoices).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.Entity<AddressInvoiceData>()
|
||||
#pragma warning disable CS0618
|
||||
.HasKey(o => o.Address);
|
||||
.HasKey(o => new { o.Address, o.PaymentMethodId });
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.App.BackupStorage;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@@ -30,9 +29,6 @@ namespace BTCPayServer.Data
|
||||
|
||||
public List<IdentityUserRole<string>> UserRoles { get; set; }
|
||||
|
||||
public List<AppStorageItemData> AppStorageItems { get; set; }
|
||||
|
||||
|
||||
public static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
|
||||
{
|
||||
builder.Entity<ApplicationUser>()
|
||||
@@ -50,5 +46,6 @@ namespace BTCPayServer.Data
|
||||
public bool ShowInvoiceStatusChangeHint { get; set; }
|
||||
public string ImageUrl { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string InvitationToken { get; set; }
|
||||
}
|
||||
}
|
||||
|
@@ -15,36 +15,13 @@ using Microsoft.EntityFrameworkCore.Diagnostics;
|
||||
using BTCPayServer.Migrations;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
public partial class InvoiceData
|
||||
public partial class InvoiceData : MigrationInterceptor.IHasMigration
|
||||
{
|
||||
/// <summary>
|
||||
/// We have a migration running in the background that will migrate the data from the old blob to the new blob
|
||||
/// Meanwhile, we need to make sure that invoices which haven't been migrated yet are migrated on the fly.
|
||||
/// </summary>
|
||||
public class MigrationInterceptor : IMaterializationInterceptor
|
||||
{
|
||||
public static readonly MigrationInterceptor Instance = new MigrationInterceptor();
|
||||
public object InitializedInstance(MaterializationInterceptionData materializationData, object entity)
|
||||
{
|
||||
if (entity is InvoiceData invoiceData && invoiceData.Currency is null)
|
||||
{
|
||||
invoiceData.Migrate();
|
||||
}
|
||||
else if (entity is PaymentData paymentData && paymentData.Currency is null)
|
||||
{
|
||||
paymentData.Migrate();
|
||||
}
|
||||
else if (entity is PayoutData payoutData && payoutData.Currency is null)
|
||||
{
|
||||
payoutData.Migrate();
|
||||
}
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
|
||||
static HashSet<string> superflousProperties = new HashSet<string>()
|
||||
{
|
||||
"availableAddressHashes",
|
||||
@@ -78,13 +55,14 @@ namespace BTCPayServer.Data
|
||||
};
|
||||
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
public void Migrate()
|
||||
public bool TryMigrate()
|
||||
{
|
||||
if (Currency is not null)
|
||||
return;
|
||||
return false;
|
||||
if (Blob is not (null or { Length: 0 }))
|
||||
{
|
||||
Blob2 = MigrationExtensions.Unzip(Blob);
|
||||
Blob2 = MigrationExtensions.SanitizeJSON(Blob2);
|
||||
Blob = null;
|
||||
}
|
||||
var blob = JObject.Parse(Blob2);
|
||||
@@ -232,11 +210,10 @@ namespace BTCPayServer.Data
|
||||
}
|
||||
|
||||
blob.ConvertNumberToString("price");
|
||||
Currency = blob["currency"].Value<string>();
|
||||
Currency = blob["currency"].Value<string>().ToUpperInvariant();
|
||||
var isTopup = blob["type"]?.Value<string>() is "TopUp";
|
||||
var amount = decimal.Parse(blob["price"].Value<string>(), CultureInfo.InvariantCulture);
|
||||
Amount = isTopup && amount == 0 ? null : decimal.Parse(blob["price"].Value<string>(), CultureInfo.InvariantCulture);
|
||||
CustomerEmail = null;
|
||||
foreach (var prop in superflousProperties)
|
||||
blob.Property(prop)?.Remove();
|
||||
if (blob["speedPolicy"] is JValue { Type: JTokenType.Integer, Value: 0 or 0L })
|
||||
@@ -373,7 +350,11 @@ namespace BTCPayServer.Data
|
||||
};
|
||||
blob["version"] = 3;
|
||||
Blob2 = blob.ToString(Formatting.None);
|
||||
return true;
|
||||
}
|
||||
|
||||
[NotMapped]
|
||||
public bool Migrated { get; set; }
|
||||
static string[] detailsRemoveDefault =
|
||||
[
|
||||
"paymentMethodFeeRate",
|
||||
|
@@ -3,6 +3,8 @@ using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
@@ -20,18 +22,17 @@ namespace BTCPayServer.Data
|
||||
[Obsolete("Use Blob2 instead")]
|
||||
public byte[] Blob { get; set; }
|
||||
public string Blob2 { get; set; }
|
||||
public string ItemCode { get; set; }
|
||||
public string OrderId { get; set; }
|
||||
public string Status { get; set; }
|
||||
public string ExceptionStatus { get; set; }
|
||||
[Obsolete("Unused")]
|
||||
public string CustomerEmail { get; set; }
|
||||
public List<AddressInvoiceData> AddressInvoices { get; set; }
|
||||
public bool Archived { get; set; }
|
||||
public List<PendingInvoiceData> PendingInvoices { get; set; }
|
||||
public List<InvoiceSearchData> InvoiceSearchData { get; set; }
|
||||
public List<RefundData> Refunds { get; set; }
|
||||
|
||||
public static string GetOrderId(string blob) => throw new NotSupportedException();
|
||||
public static string GetItemCode(string blob) => throw new NotSupportedException();
|
||||
public static bool IsPending(string status) => throw new NotSupportedException();
|
||||
|
||||
[Timestamp]
|
||||
// With this, update of InvoiceData will fail if the row was modified by another process
|
||||
public uint XMin { get; set; }
|
||||
@@ -41,7 +42,6 @@ namespace BTCPayServer.Data
|
||||
.HasOne(o => o.StoreData)
|
||||
.WithMany(a => a.Invoices).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.Entity<InvoiceData>().HasIndex(o => o.StoreDataId);
|
||||
builder.Entity<InvoiceData>().HasIndex(o => o.OrderId);
|
||||
builder.Entity<InvoiceData>().HasIndex(o => o.Created);
|
||||
builder.Entity<InvoiceData>()
|
||||
.Property(o => o.Blob2)
|
||||
@@ -49,6 +49,9 @@ namespace BTCPayServer.Data
|
||||
builder.Entity<InvoiceData>()
|
||||
.Property(o => o.Amount)
|
||||
.HasColumnType("NUMERIC");
|
||||
builder.HasDbFunction(typeof(InvoiceData).GetMethod(nameof(GetOrderId), new[] { typeof(string) }), b => b.HasName("get_orderid"));
|
||||
builder.HasDbFunction(typeof(InvoiceData).GetMethod(nameof(GetItemCode), new[] { typeof(string) }), b => b.HasName("get_itemcode"));
|
||||
builder.HasDbFunction(typeof(InvoiceData).GetMethod(nameof(IsPending), new[] { typeof(string) }), b => b.HasName("is_pending"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -147,5 +147,8 @@ namespace BTCPayServer.Data
|
||||
return $"{splitted[0]}-CHAIN";
|
||||
throw new NotSupportedException("Unknown payment id " + paymentMethodId);
|
||||
}
|
||||
|
||||
// Make postgres happy
|
||||
public static string SanitizeJSON(string json) => json.Replace("\\u0000", string.Empty, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
45
BTCPayServer.Data/Data/MigrationInterceptor.cs
Normal file
45
BTCPayServer.Data/Data/MigrationInterceptor.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.EntityFrameworkCore.Diagnostics;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
/// <summary>
|
||||
/// We have a migration running in the background that will migrate the data from the old blob to the new blob
|
||||
/// Meanwhile, we need to make sure that invoices which haven't been migrated yet are migrated on the fly.
|
||||
/// </summary>
|
||||
public class MigrationInterceptor : IMaterializationInterceptor, ISaveChangesInterceptor
|
||||
{
|
||||
public interface IHasMigration
|
||||
{
|
||||
bool TryMigrate();
|
||||
bool Migrated { get; set; }
|
||||
}
|
||||
public static readonly MigrationInterceptor Instance = new MigrationInterceptor();
|
||||
public object InitializedInstance(MaterializationInterceptionData materializationData, object entity)
|
||||
{
|
||||
if (entity is IHasMigration hasMigration && hasMigration.TryMigrate())
|
||||
{
|
||||
hasMigration.Migrated = true;
|
||||
}
|
||||
return entity;
|
||||
}
|
||||
|
||||
public ValueTask<InterceptionResult<int>> SavingChangesAsync(DbContextEventData eventData, InterceptionResult<int> result, CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
|
||||
foreach (var entry in eventData.Context.ChangeTracker.Entries())
|
||||
{
|
||||
if (entry is { Entity: IHasMigration { Migrated: true }, State: EntityState.Modified })
|
||||
// It seems doing nothing, but this actually set all properties as modified
|
||||
entry.State = EntityState.Modified;
|
||||
}
|
||||
return new ValueTask<InterceptionResult<int>>(result);
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,10 +1,13 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Linq;
|
||||
using System.Reflection.Metadata;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.ChangeTracking.Internal;
|
||||
using NBitcoin;
|
||||
using NBitcoin.Altcoins;
|
||||
using NBitcoin.DataEncoders;
|
||||
@@ -13,16 +16,17 @@ using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
public partial class PaymentData
|
||||
public partial class PaymentData : MigrationInterceptor.IHasMigration
|
||||
{
|
||||
public void Migrate()
|
||||
public bool TryMigrate()
|
||||
{
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
if (Currency is not null)
|
||||
return;
|
||||
return false;
|
||||
if (Blob is not (null or { Length: 0 }))
|
||||
{
|
||||
Blob2 = MigrationExtensions.Unzip(Blob);
|
||||
Blob2 = MigrationExtensions.SanitizeJSON(Blob2);
|
||||
Blob = null;
|
||||
}
|
||||
var blob = JObject.Parse(Blob2);
|
||||
@@ -42,9 +46,10 @@ namespace BTCPayServer.Data
|
||||
}
|
||||
|
||||
var cryptoCode = blob["cryptoCode"].Value<string>();
|
||||
Type = cryptoCode + "_" + blob["cryptoPaymentDataType"].Value<string>();
|
||||
Type = MigrationExtensions.MigratePaymentMethodId(Type);
|
||||
var divisibility = MigrationExtensions.GetDivisibility(Type);
|
||||
MigratedPaymentMethodId = PaymentMethodId;
|
||||
PaymentMethodId = cryptoCode + "_" + blob["cryptoPaymentDataType"].Value<string>();
|
||||
PaymentMethodId = MigrationExtensions.MigratePaymentMethodId(PaymentMethodId);
|
||||
var divisibility = MigrationExtensions.GetDivisibility(PaymentMethodId);
|
||||
Currency = blob["cryptoCode"].Value<string>();
|
||||
blob.Remove("cryptoCode");
|
||||
blob.Remove("cryptoPaymentDataType");
|
||||
@@ -157,7 +162,13 @@ namespace BTCPayServer.Data
|
||||
Blob2 = blob.ToString(Formatting.None);
|
||||
Accounted = null;
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
return true;
|
||||
}
|
||||
[NotMapped]
|
||||
public bool Migrated { get; set; }
|
||||
[NotMapped]
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public string MigratedPaymentMethodId { get; set; }
|
||||
|
||||
static readonly DateTimeOffset unixRef = new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero);
|
||||
public static long DateTimeToMilliUnixTime(in DateTime time)
|
||||
|
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
|
||||
@@ -26,13 +27,15 @@ namespace BTCPayServer.Data
|
||||
[Obsolete("Use Blob2 instead")]
|
||||
public byte[] Blob { get; set; }
|
||||
public string Blob2 { get; set; }
|
||||
public string Type { get; set; }
|
||||
public string PaymentMethodId { get; set; }
|
||||
[Obsolete("Use Status instead")]
|
||||
public bool? Accounted { get; set; }
|
||||
public PaymentStatus? Status { get; set; }
|
||||
|
||||
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
|
||||
public static bool IsPending(PaymentStatus? status) => throw new NotSupportedException();
|
||||
internal static void OnModelCreating(ModelBuilder builder)
|
||||
{
|
||||
builder.Entity<PaymentData>()
|
||||
.HasKey(o => new { o.Id, o.PaymentMethodId });
|
||||
builder.Entity<PaymentData>()
|
||||
.HasOne(o => o.InvoiceData)
|
||||
.WithMany(i => i.Payments).OnDelete(DeleteBehavior.Cascade);
|
||||
@@ -47,6 +50,7 @@ namespace BTCPayServer.Data
|
||||
builder.Entity<PaymentData>()
|
||||
.Property(o => o.Amount)
|
||||
.HasColumnType("NUMERIC");
|
||||
builder.HasDbFunction(typeof(PaymentData).GetMethod(nameof(IsPending), new[] { typeof(PaymentStatus?) }), b => b.HasName("is_pending"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,18 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
public partial class PayoutData
|
||||
{
|
||||
public void Migrate()
|
||||
{
|
||||
PayoutMethodId = MigrationExtensions.MigratePaymentMethodId(PayoutMethodId);
|
||||
// Could only be BTC-LN or BTC-CHAIN, so we extract the crypto currency
|
||||
Currency = PayoutMethodId.Split('-')[0];
|
||||
}
|
||||
}
|
||||
}
|
@@ -18,7 +18,23 @@ namespace BTCPayServer.Data
|
||||
public DateTimeOffset Date { get; set; }
|
||||
public string PullPaymentDataId { get; set; }
|
||||
public string StoreDataId { get; set; }
|
||||
/// <summary>
|
||||
/// The currency of the payout (eg. BTC)
|
||||
/// </summary>
|
||||
public string Currency { get; set; }
|
||||
/// <summary>
|
||||
/// The amount of the payout in Currency.
|
||||
/// The Amount only get set when the payout is actually approved.
|
||||
/// </summary>
|
||||
public decimal? Amount { get; set; }
|
||||
/// <summary>
|
||||
/// The original currency of the payout (eg. USD)
|
||||
/// </summary>
|
||||
public string OriginalCurrency { get; set; }
|
||||
/// <summary>
|
||||
/// The amount of the payout in OriginalCurrency
|
||||
/// </summary>
|
||||
public decimal OriginalAmount { get; set; }
|
||||
public PullPaymentData PullPaymentData { get; set; }
|
||||
[MaxLength(20)]
|
||||
public PayoutState State { get; set; }
|
||||
@@ -28,7 +44,14 @@ namespace BTCPayServer.Data
|
||||
public string Blob { get; set; }
|
||||
public string Proof { get; set; }
|
||||
#nullable enable
|
||||
public string? Destination { get; set; }
|
||||
/// <summary>
|
||||
/// For example, BTC-CHAIN needs to ensure that only a single address is tied to an active payout.
|
||||
/// If `PayoutBlob.Destination` is `bitcoin://1BvBMSeYstWetqTFn5Au4m4GFg7xJaNVN2?amount=0.1`
|
||||
/// Then `DedupId` is `1BvBMSeYstWetqTFn5Au4m4GFg7xJaNVN2`
|
||||
/// For Lightning, Destination could be the lightning address, BOLT11 or LNURL
|
||||
/// But the `DedupId` would be the `PaymentHash`.
|
||||
/// </summary>
|
||||
public string? DedupId { get; set; }
|
||||
#nullable restore
|
||||
public StoreData StoreData { get; set; }
|
||||
|
||||
@@ -46,7 +69,7 @@ namespace BTCPayServer.Data
|
||||
builder.Entity<PayoutData>()
|
||||
.HasIndex(o => o.State);
|
||||
builder.Entity<PayoutData>()
|
||||
.HasIndex(x => new { DestinationId = x.Destination, x.State });
|
||||
.HasIndex(x => new { DestinationId = x.DedupId, x.State });
|
||||
|
||||
builder.Entity<PayoutData>()
|
||||
.Property(o => o.Blob)
|
||||
|
@@ -16,7 +16,7 @@ public class PayoutProcessorData : IHasBlobUntyped
|
||||
public string Id { get; set; }
|
||||
public string StoreId { get; set; }
|
||||
public StoreData Store { get; set; }
|
||||
public string PaymentMethod { get; set; }
|
||||
public string PayoutMethodId { get; set; }
|
||||
public string Processor { get; set; }
|
||||
|
||||
[Obsolete("Use Blob2 instead")]
|
||||
@@ -36,6 +36,6 @@ public class PayoutProcessorData : IHasBlobUntyped
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Processor} {PaymentMethod} {StoreId}";
|
||||
return $"{Processor} {PayoutMethodId} {StoreId}";
|
||||
}
|
||||
}
|
||||
|
@@ -1,18 +0,0 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
public class PendingInvoiceData
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public InvoiceData InvoiceData { get; set; }
|
||||
|
||||
internal static void OnModelCreating(ModelBuilder builder)
|
||||
{
|
||||
builder.Entity<PendingInvoiceData>()
|
||||
.HasOne(o => o.InvoiceData)
|
||||
.WithMany(o => o.PendingInvoices)
|
||||
.HasForeignKey(o => o.Id).OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
}
|
||||
}
|
@@ -24,6 +24,8 @@ namespace BTCPayServer.Data
|
||||
public StoreData StoreData { get; set; }
|
||||
[MaxLength(50)]
|
||||
public string StoreId { get; set; }
|
||||
public string Currency { get; set; }
|
||||
public decimal Limit { get; set; }
|
||||
public DateTimeOffset StartDate { get; set; }
|
||||
public DateTimeOffset? EndDate { get; set; }
|
||||
public bool Archived { get; set; }
|
||||
|
@@ -1,30 +0,0 @@
|
||||
using System;
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20240405052858_cleanup_address_invoices")]
|
||||
public partial class cleanup_address_invoices : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.Sql(@"
|
||||
DELETE FROM ""AddressInvoices"" WHERE ""Address"" LIKE '%_LightningLike';
|
||||
ALTER TABLE ""AddressInvoices"" DROP COLUMN IF EXISTS ""CreatedTime"";
|
||||
");
|
||||
migrationBuilder.Sql(@"VACUUM (FULL, ANALYZE) ""AddressInvoices"";", true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,29 +0,0 @@
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20240520042729_payoutsmigration")]
|
||||
public partial class payoutsmigration : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Currency",
|
||||
table: "Payouts",
|
||||
type: "text",
|
||||
nullable: true);
|
||||
migrationBuilder.RenameColumn("PaymentMethodId", "Payouts", "PayoutMethodId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@@ -1,56 +0,0 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AppStuff : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AppStorageItems",
|
||||
columns: table => new
|
||||
{
|
||||
Key = table.Column<string>(type: "text", nullable: false),
|
||||
Version = table.Column<long>(type: "bigint", nullable: false),
|
||||
UserId = table.Column<string>(type: "text", nullable: false),
|
||||
Value = table.Column<byte[]>(type: "bytea", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AppStorageItems", x => new { x.Key, x.Version, x.UserId });
|
||||
table.ForeignKey(
|
||||
name: "FK_AppStorageItems_AspNetUsers_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AppStorageItems_Key_UserId",
|
||||
table: "AppStorageItems",
|
||||
columns: new[] { "Key", "UserId" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AppStorageItems_UserId",
|
||||
table: "AppStorageItems",
|
||||
column: "UserId");
|
||||
|
||||
migrationBuilder.Sql("CREATE FUNCTION \"LC_TRIGGER_BEFORE_INSERT_APPSTORAGEITEMDATA\"() RETURNS trigger as $LC_TRIGGER_BEFORE_INSERT_APPSTORAGEITEMDATA$\r\nBEGIN\r\n DELETE FROM \"AppStorageItems\"\r\n WHERE NEW.\"UserId\" = \"AppStorageItems\".\"UserId\" AND NEW.\"Key\" = \"AppStorageItems\".\"Key\" AND NEW.\"Version\" > \"AppStorageItems\".\"Version\";\r\nRETURN NEW;\r\nEND;\r\n$LC_TRIGGER_BEFORE_INSERT_APPSTORAGEITEMDATA$ LANGUAGE plpgsql;\r\nCREATE TRIGGER LC_TRIGGER_BEFORE_INSERT_APPSTORAGEITEMDATA BEFORE INSERT\r\nON \"AppStorageItems\"\r\nFOR EACH ROW EXECUTE PROCEDURE \"LC_TRIGGER_BEFORE_INSERT_APPSTORAGEITEMDATA\"();");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.Sql("DROP FUNCTION \"LC_TRIGGER_BEFORE_INSERT_APPSTORAGEITEMDATA\"() CASCADE;");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AppStorageItems");
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,35 @@
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20240826065950_removeinvoicecols")]
|
||||
[DBScript("001.InvoiceFunctions.sql")]
|
||||
public partial class removeinvoicecols : DBScriptsMigration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Invoices_OrderId",
|
||||
table: "Invoices");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ItemCode",
|
||||
table: "Invoices");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "OrderId",
|
||||
table: "Invoices");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CustomerEmail",
|
||||
table: "Invoices");
|
||||
base.Up(migrationBuilder);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,40 @@
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20240827034505_migratepayouts")]
|
||||
[DBScript("002.RefactorPayouts.sql")]
|
||||
public partial class migratepayouts : DBScriptsMigration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
base.Up(migrationBuilder);
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "Destination",
|
||||
table: "Payouts",
|
||||
newName: "DedupId");
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "IX_Payouts_Destination_State",
|
||||
table: "Payouts",
|
||||
newName: "IX_Payouts_DedupId_State");
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "PaymentMethod",
|
||||
table: "PayoutProcessors",
|
||||
newName: "PayoutMethodId");
|
||||
|
||||
migrationBuilder.Sql("""
|
||||
UPDATE "PayoutProcessors"
|
||||
SET
|
||||
"PayoutMethodId" = CASE WHEN STRPOS("PayoutMethodId", '_') = 0 THEN "PayoutMethodId" || '-CHAIN'
|
||||
WHEN STRPOS("PayoutMethodId", '_LightningLike') > 0 THEN split_part("PayoutMethodId", '_LightningLike', 1) || '-LN'
|
||||
WHEN STRPOS("PayoutMethodId", '_LNURLPAY') > 0 THEN split_part("PayoutMethodId",'_LNURLPAY', 1) || '-LN'
|
||||
ELSE "PayoutMethodId" END
|
||||
""");
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,54 @@
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20240904092905_UpdateStoreOwnerRole")]
|
||||
public partial class UpdateStoreOwnerRole : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.UpdateData(
|
||||
"StoreRoles",
|
||||
keyColumns: new[] { "Id" },
|
||||
keyColumnTypes: new[] { "TEXT" },
|
||||
keyValues: new[] { "Owner" },
|
||||
columns: new[] { "Permissions" },
|
||||
columnTypes: new[] { "TEXT[]" },
|
||||
values: new object[]
|
||||
{
|
||||
new[]
|
||||
{
|
||||
"btcpay.store.canmodifystoresettings"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.UpdateData(
|
||||
"StoreRoles",
|
||||
keyColumns: new[] { "Id" },
|
||||
keyColumnTypes: new[] { "TEXT" },
|
||||
keyValues: new[] { "Owner" },
|
||||
columns: new[] { "Permissions" },
|
||||
columnTypes: new[] { "TEXT[]" },
|
||||
values: new object[]
|
||||
{
|
||||
new[]
|
||||
{
|
||||
"btcpay.store.canmodifystoresettings",
|
||||
"btcpay.store.cantradecustodianaccount",
|
||||
"btcpay.store.canwithdrawfromcustodianaccount",
|
||||
"btcpay.store.candeposittocustodianaccount"
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,15 @@
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20240913034505_refactorpendinginvoicespayments")]
|
||||
[DBScript("003.RefactorPendingInvoicesPayments.sql")]
|
||||
public partial class refactorpendinginvoicespayments : DBScriptsMigration
|
||||
{
|
||||
}
|
||||
}
|
@@ -0,0 +1,80 @@
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20240919085726_refactorinvoiceaddress")]
|
||||
public partial class refactorinvoiceaddress : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropPrimaryKey(
|
||||
name: "PK_AddressInvoices",
|
||||
table: "AddressInvoices");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "PaymentMethodId",
|
||||
table: "AddressInvoices",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
|
||||
migrationBuilder.Sql("""
|
||||
UPDATE "AddressInvoices"
|
||||
SET
|
||||
"Address" = (string_to_array("Address", '#'))[1],
|
||||
"PaymentMethodId" = CASE WHEN (string_to_array("Address", '#'))[2] IS NULL THEN 'BTC-CHAIN'
|
||||
WHEN STRPOS((string_to_array("Address", '#'))[2], '_') = 0 THEN (string_to_array("Address", '#'))[2] || '-CHAIN'
|
||||
WHEN STRPOS((string_to_array("Address", '#'))[2], '_MoneroLike') > 0 THEN replace((string_to_array("Address", '#'))[2],'_MoneroLike','-CHAIN')
|
||||
WHEN STRPOS((string_to_array("Address", '#'))[2], '_ZcashLike') > 0 THEN replace((string_to_array("Address", '#'))[2],'_ZcashLike','-CHAIN')
|
||||
ELSE '' END;
|
||||
ALTER TABLE "AddressInvoices" DROP COLUMN IF EXISTS "CreatedTime";
|
||||
DELETE FROM "AddressInvoices" WHERE "PaymentMethodId" = '';
|
||||
""");
|
||||
migrationBuilder.AddPrimaryKey(
|
||||
name: "PK_AddressInvoices",
|
||||
table: "AddressInvoices",
|
||||
columns: new[] { "Address", "PaymentMethodId" });
|
||||
migrationBuilder.Sql("VACUUM (ANALYZE) \"AddressInvoices\";", true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropPrimaryKey(
|
||||
name: "PK_AddressInvoices",
|
||||
table: "AddressInvoices");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "PaymentMethodId",
|
||||
table: "AddressInvoices");
|
||||
|
||||
migrationBuilder.AddPrimaryKey(
|
||||
name: "PK_AddressInvoices",
|
||||
table: "AddressInvoices",
|
||||
column: "Address");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "PendingInvoices",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(type: "text", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_PendingInvoices", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_PendingInvoices_Invoices_Id",
|
||||
column: x => x.Id,
|
||||
principalTable: "Invoices",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,38 @@
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20240923065254_refactorpayments")]
|
||||
public partial class refactorpayments : DBScriptsMigration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropPrimaryKey(
|
||||
name: "PK_Payments",
|
||||
table: "Payments");
|
||||
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "Type",
|
||||
table: "Payments",
|
||||
newName: "PaymentMethodId");
|
||||
migrationBuilder.Sql("UPDATE \"Payments\" SET \"PaymentMethodId\"='' WHERE \"PaymentMethodId\" IS NULL;");
|
||||
migrationBuilder.AddPrimaryKey(
|
||||
name: "PK_Payments",
|
||||
table: "Payments",
|
||||
columns: new[] { "Id", "PaymentMethodId" });
|
||||
base.Up(migrationBuilder);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,15 @@
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20240924065254_monitoredinvoices")]
|
||||
[DBScript("004.MonitoredInvoices.sql")]
|
||||
public partial class monitoredinvoices : DBScriptsMigration
|
||||
{
|
||||
}
|
||||
}
|
@@ -23,35 +23,6 @@ namespace BTCPayServer.Migrations
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.App.BackupStorage.AppStorageItemData", b =>
|
||||
{
|
||||
b.Property<string>("Key")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<long>("Version")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<byte[]>("Value")
|
||||
.HasColumnType("bytea");
|
||||
|
||||
b.HasKey("Key", "Version", "UserId");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.HasIndex("Key", "UserId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("AppStorageItems", t =>
|
||||
{
|
||||
t.HasTrigger("LC_TRIGGER_BEFORE_INSERT_APPSTORAGEITEMDATA");
|
||||
});
|
||||
|
||||
b.HasAnnotation("LC_TRIGGER_BEFORE_INSERT_APPSTORAGEITEMDATA", "CREATE FUNCTION \"LC_TRIGGER_BEFORE_INSERT_APPSTORAGEITEMDATA\"() RETURNS trigger as $LC_TRIGGER_BEFORE_INSERT_APPSTORAGEITEMDATA$\r\nBEGIN\r\n DELETE FROM \"AppStorageItems\"\r\n WHERE NEW.\"UserId\" = \"AppStorageItems\".\"UserId\" AND NEW.\"Key\" = \"AppStorageItems\".\"Key\" AND NEW.\"Version\" > \"AppStorageItems\".\"Version\";\r\nRETURN NEW;\r\nEND;\r\n$LC_TRIGGER_BEFORE_INSERT_APPSTORAGEITEMDATA$ LANGUAGE plpgsql;\r\nCREATE TRIGGER LC_TRIGGER_BEFORE_INSERT_APPSTORAGEITEMDATA BEFORE INSERT\r\nON \"AppStorageItems\"\r\nFOR EACH ROW EXECUTE PROCEDURE \"LC_TRIGGER_BEFORE_INSERT_APPSTORAGEITEMDATA\"();");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
@@ -92,10 +63,13 @@ namespace BTCPayServer.Migrations
|
||||
b.Property<string>("Address")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("PaymentMethodId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("InvoiceDataId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Address");
|
||||
b.HasKey("Address", "PaymentMethodId");
|
||||
|
||||
b.HasIndex("InvoiceDataId");
|
||||
|
||||
@@ -296,18 +270,9 @@ namespace BTCPayServer.Migrations
|
||||
b.Property<string>("Currency")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("CustomerEmail")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ExceptionStatus")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ItemCode")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("OrderId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.HasColumnType("text");
|
||||
|
||||
@@ -324,8 +289,6 @@ namespace BTCPayServer.Migrations
|
||||
|
||||
b.HasIndex("Created");
|
||||
|
||||
b.HasIndex("OrderId");
|
||||
|
||||
b.HasIndex("StoreDataId");
|
||||
|
||||
b.ToTable("Invoices");
|
||||
@@ -519,6 +482,9 @@ namespace BTCPayServer.Migrations
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("PaymentMethodId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool?>("Accounted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
@@ -543,10 +509,7 @@ namespace BTCPayServer.Migrations
|
||||
b.Property<string>("Status")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
b.HasKey("Id", "PaymentMethodId");
|
||||
|
||||
b.HasIndex("InvoiceDataId");
|
||||
|
||||
@@ -593,6 +556,9 @@ namespace BTCPayServer.Migrations
|
||||
.HasMaxLength(30)
|
||||
.HasColumnType("character varying(30)");
|
||||
|
||||
b.Property<decimal?>("Amount")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<string>("Blob")
|
||||
.HasColumnType("JSONB");
|
||||
|
||||
@@ -602,7 +568,13 @@ namespace BTCPayServer.Migrations
|
||||
b.Property<DateTimeOffset>("Date")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Destination")
|
||||
b.Property<string>("DedupId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<decimal>("OriginalAmount")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<string>("OriginalCurrency")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("PayoutMethodId")
|
||||
@@ -632,7 +604,7 @@ namespace BTCPayServer.Migrations
|
||||
|
||||
b.HasIndex("StoreDataId");
|
||||
|
||||
b.HasIndex("Destination", "State");
|
||||
b.HasIndex("DedupId", "State");
|
||||
|
||||
b.ToTable("Payouts");
|
||||
});
|
||||
@@ -649,7 +621,7 @@ namespace BTCPayServer.Migrations
|
||||
b.Property<string>("Blob2")
|
||||
.HasColumnType("JSONB");
|
||||
|
||||
b.Property<string>("PaymentMethod")
|
||||
b.Property<string>("PayoutMethodId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Processor")
|
||||
@@ -665,16 +637,6 @@ namespace BTCPayServer.Migrations
|
||||
b.ToTable("PayoutProcessors");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("PendingInvoices");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PlannedTransaction", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
@@ -704,9 +666,15 @@ namespace BTCPayServer.Migrations
|
||||
b.Property<string>("Blob")
|
||||
.HasColumnType("JSONB");
|
||||
|
||||
b.Property<string>("Currency")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTimeOffset?>("EndDate")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<decimal>("Limit")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<DateTimeOffset>("StartDate")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
@@ -1181,17 +1149,6 @@ namespace BTCPayServer.Migrations
|
||||
b.ToTable("AspNetUserTokens", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.App.BackupStorage.AppStorageItemData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.ApplicationUser", "User")
|
||||
.WithMany("AppStorageItems")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
@@ -1367,17 +1324,6 @@ namespace BTCPayServer.Migrations
|
||||
b.Navigation("Store");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany("PendingInvoices")
|
||||
.HasForeignKey("Id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("InvoiceData");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PullPaymentData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
@@ -1587,8 +1533,6 @@ namespace BTCPayServer.Migrations
|
||||
{
|
||||
b.Navigation("APIKeys");
|
||||
|
||||
b.Navigation("AppStorageItems");
|
||||
|
||||
b.Navigation("Fido2Credentials");
|
||||
|
||||
b.Navigation("Notifications");
|
||||
@@ -1610,8 +1554,6 @@ namespace BTCPayServer.Migrations
|
||||
|
||||
b.Navigation("Payments");
|
||||
|
||||
b.Navigation("PendingInvoices");
|
||||
|
||||
b.Navigation("Refunds");
|
||||
});
|
||||
|
||||
|
@@ -34,16 +34,13 @@
|
||||
<PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.15" />
|
||||
<PackageReference Include="Selenium.Support" Version="4.1.1" />
|
||||
<PackageReference Include="Selenium.WebDriver" Version="4.22.0" />
|
||||
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="125.0.6422.14100" />
|
||||
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="128.0.6613.11900" />
|
||||
<PackageReference Include="xunit" Version="2.6.6" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.6">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(Altcoins)' != 'true'">
|
||||
<Compile Remove="AltcoinTests\**\*.cs"></Compile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Update=".dockerignore">
|
||||
<DependentUpon>Dockerfile</DependentUpon>
|
||||
|
@@ -31,12 +31,6 @@ using AuthenticationSchemes = BTCPayServer.Abstractions.Constants.Authentication
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
public enum TestDatabases
|
||||
{
|
||||
Postgres,
|
||||
MySQL,
|
||||
}
|
||||
|
||||
public class BTCPayServerTester : IDisposable
|
||||
{
|
||||
internal readonly string _Directory;
|
||||
@@ -69,11 +63,6 @@ namespace BTCPayServer.Tests
|
||||
set;
|
||||
}
|
||||
|
||||
public string MySQL
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string Postgres
|
||||
{
|
||||
get; set;
|
||||
@@ -89,11 +78,6 @@ namespace BTCPayServer.Tests
|
||||
get; set;
|
||||
}
|
||||
|
||||
public TestDatabases TestDatabase
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public async Task RestartStartupTask<T>()
|
||||
{
|
||||
var startupTask = GetService<IServiceProvider>().GetServices<Abstractions.Contracts.IStartupTask>()
|
||||
@@ -164,9 +148,7 @@ namespace BTCPayServer.Tests
|
||||
if (!string.IsNullOrEmpty(SSHConnection))
|
||||
config.AppendLine($"sshconnection={SSHConnection}");
|
||||
|
||||
if (TestDatabase == TestDatabases.MySQL && !String.IsNullOrEmpty(MySQL))
|
||||
config.AppendLine($"mysql=" + MySQL);
|
||||
else if (!String.IsNullOrEmpty(Postgres))
|
||||
if (!String.IsNullOrEmpty(Postgres))
|
||||
config.AppendLine($"postgres=" + Postgres);
|
||||
|
||||
if (!string.IsNullOrEmpty(ExplorerPostgres))
|
||||
@@ -195,7 +177,7 @@ namespace BTCPayServer.Tests
|
||||
l.AddFilter("System.Net.Http.HttpClient", LogLevel.Critical);
|
||||
l.SetMinimumLevel(LogLevel.Information)
|
||||
.AddFilter("Microsoft", LogLevel.Error)
|
||||
.AddFilter("Hangfire", LogLevel.Error)
|
||||
.AddFilter("Microsoft.EntityFrameworkCore.Migrations", LogLevel.Information)
|
||||
.AddFilter("Fido2NetLib.DistributedCacheMetadataService", LogLevel.Error)
|
||||
.AddProvider(LoggerProvider);
|
||||
});
|
||||
|
@@ -58,8 +58,8 @@ namespace BTCPayServer.Tests
|
||||
var qrValue = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-qr-value");
|
||||
var clipboard = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
|
||||
var payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href");
|
||||
var address = s.Driver.FindElement(By.CssSelector("#Address_BTC-CHAIN .truncate-center-start")).Text;
|
||||
Assert.StartsWith("bcrt", s.Driver.FindElement(By.CssSelector("#Address_BTC-CHAIN .truncate-center-start")).Text);
|
||||
var address = s.Driver.FindElement(By.CssSelector("#Address_BTC-CHAIN .truncate-center")).GetAttribute("data-text");
|
||||
Assert.StartsWith("bcrt", address);
|
||||
Assert.DoesNotContain("lightning=", payUrl);
|
||||
Assert.Equal($"bitcoin:{address}", payUrl);
|
||||
Assert.Equal($"bitcoin:{address}", clipboard);
|
||||
@@ -80,7 +80,7 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href");
|
||||
Assert.StartsWith("lightning:lnurl", payUrl);
|
||||
Assert.StartsWith("lnurl", s.Driver.WaitForElement(By.CssSelector("#Lightning_BTC-CHAIN .truncate-center-start")).Text);
|
||||
Assert.StartsWith("lnurl", s.Driver.WaitForElement(By.CssSelector("#Lightning_BTC-CHAIN .truncate-center")).GetAttribute("data-text"));
|
||||
s.Driver.ElementDoesNotExist(By.Id("Address_BTC-CHAIN"));
|
||||
});
|
||||
|
||||
@@ -94,7 +94,7 @@ namespace BTCPayServer.Tests
|
||||
qrValue = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-qr-value");
|
||||
clipboard = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
|
||||
payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href");
|
||||
address = s.Driver.FindElement(By.CssSelector("#Lightning_BTC-LN .truncate-center-start")).Text;
|
||||
address = s.Driver.FindElement(By.CssSelector("#Lightning_BTC-LN .truncate-center")).GetAttribute("data-text");
|
||||
Assert.Equal($"lightning:{address}", payUrl);
|
||||
Assert.Equal($"lightning:{address}", clipboard);
|
||||
Assert.Equal($"lightning:{address.ToUpperInvariant()}", qrValue);
|
||||
@@ -147,7 +147,7 @@ namespace BTCPayServer.Tests
|
||||
invoiceId = s.CreateInvoice(2100, "EUR");
|
||||
s.GoToInvoiceCheckout(invoiceId);
|
||||
await Task.Delay(200);
|
||||
address = s.Driver.FindElement(By.CssSelector("#Address_BTC-CHAIN .truncate-center-start")).Text;
|
||||
address = s.Driver.FindElement(By.CssSelector("#Address_BTC-CHAIN .truncate-center")).GetAttribute("data-text");
|
||||
var amountFraction = "0.00001";
|
||||
await s.Server.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create(address, Network.RegTest),
|
||||
Money.Parse(amountFraction));
|
||||
@@ -261,8 +261,8 @@ namespace BTCPayServer.Tests
|
||||
qrValue = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-qr-value");
|
||||
clipboard = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
|
||||
payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href");
|
||||
var copyAddressOnchain = s.Driver.FindElement(By.CssSelector("#Address_BTC-CHAIN .truncate-center-start")).Text;
|
||||
var copyAddressLightning = s.Driver.FindElement(By.CssSelector("#Lightning_BTC-CHAIN .truncate-center-start")).Text;
|
||||
var copyAddressOnchain = s.Driver.FindElement(By.CssSelector("#Address_BTC-CHAIN .truncate-center")).GetAttribute("data-text");
|
||||
var copyAddressLightning = s.Driver.FindElement(By.CssSelector("#Lightning_BTC-CHAIN .truncate-center")).GetAttribute("data-text");
|
||||
Assert.StartsWith($"bitcoin:{copyAddressOnchain}?amount=", payUrl);
|
||||
Assert.Contains("?amount=", payUrl);
|
||||
Assert.Contains("&lightning=", payUrl);
|
||||
@@ -327,8 +327,8 @@ namespace BTCPayServer.Tests
|
||||
qrValue = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-qr-value");
|
||||
clipboard = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
|
||||
payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href");
|
||||
copyAddressOnchain = s.Driver.FindElement(By.CssSelector("#Address_BTC-CHAIN .truncate-center-start")).Text;
|
||||
copyAddressLightning = s.Driver.FindElement(By.CssSelector("#Lightning_BTC-CHAIN .truncate-center-start")).Text;
|
||||
copyAddressOnchain = s.Driver.FindElement(By.CssSelector("#Address_BTC-CHAIN .truncate-center")).GetAttribute("data-text");
|
||||
copyAddressLightning = s.Driver.FindElement(By.CssSelector("#Lightning_BTC-CHAIN .truncate-center")).GetAttribute("data-text");
|
||||
Assert.StartsWith($"bitcoin:{copyAddressOnchain}", payUrl);
|
||||
Assert.Contains("?lightning=lnurl", payUrl);
|
||||
Assert.DoesNotContain("amount=", payUrl);
|
||||
|
101
BTCPayServer.Tests/DatabaseTester.cs
Normal file
101
BTCPayServer.Tests/DatabaseTester.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
using System;
|
||||
using System.Data;
|
||||
using System.Data.Common;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
using Dapper;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NBitcoin;
|
||||
using Npgsql;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
public class DatabaseTester
|
||||
{
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
private readonly string dbname;
|
||||
private string[] notAppliedMigrations;
|
||||
|
||||
public DatabaseTester(ILog log, ILoggerFactory loggerFactory)
|
||||
{
|
||||
var connStr = Environment.GetEnvironmentVariable("TESTS_POSTGRES");
|
||||
if (string.IsNullOrEmpty(connStr))
|
||||
connStr = ServerTester.DefaultConnectionString;
|
||||
var r = RandomUtils.GetUInt32();
|
||||
dbname = $"btcpayserver{r}";
|
||||
connStr = connStr.Replace("btcpayserver", dbname);
|
||||
log.LogInformation("DB: " + dbname);
|
||||
ConnectionString = connStr;
|
||||
_loggerFactory = loggerFactory;
|
||||
}
|
||||
|
||||
public ApplicationDbContextFactory CreateContextFactory()
|
||||
{
|
||||
return new ApplicationDbContextFactory(new OptionsWrapper<DatabaseOptions>(new DatabaseOptions()
|
||||
{
|
||||
ConnectionString = ConnectionString
|
||||
}), _loggerFactory);
|
||||
}
|
||||
|
||||
public InvoiceRepository GetInvoiceRepository()
|
||||
{
|
||||
var logs = new BTCPayServer.Logging.Logs();
|
||||
logs.Configure(_loggerFactory);
|
||||
return new InvoiceRepository(CreateContextFactory(), new EventAggregator(logs));
|
||||
}
|
||||
|
||||
public ApplicationDbContext CreateContext() => CreateContextFactory().CreateContext();
|
||||
|
||||
public async Task MigrateAsync()
|
||||
{
|
||||
using var ctx = CreateContext();
|
||||
await EnsureCreatedAsync();
|
||||
await ctx.Database.MigrateAsync();
|
||||
}
|
||||
|
||||
private async Task EnsureCreatedAsync()
|
||||
{
|
||||
var builder = new Npgsql.NpgsqlConnectionStringBuilder(ConnectionString);
|
||||
builder.Database = null;
|
||||
NpgsqlConnection conn = new NpgsqlConnection(builder.ToString());
|
||||
await conn.ExecuteAsync($"CREATE DATABASE \"{dbname}\";");
|
||||
}
|
||||
|
||||
public async Task MigrateUntil(string migration = null)
|
||||
{
|
||||
using var ctx = CreateContext();
|
||||
var db = ctx.Database.GetDbConnection();
|
||||
await EnsureCreatedAsync();
|
||||
var migrations = ctx.Database.GetMigrations().ToArray();
|
||||
if (migration is not null)
|
||||
{
|
||||
var untilMigrationIdx = Array.IndexOf(migrations, migration);
|
||||
if (untilMigrationIdx == -1)
|
||||
throw new InvalidOperationException($"Migration {migration} not found");
|
||||
notAppliedMigrations = migrations[untilMigrationIdx..];
|
||||
await db.ExecuteAsync("CREATE TABLE IF NOT EXISTS \"__EFMigrationsHistory\" (\"MigrationId\" TEXT, \"ProductVersion\" TEXT)");
|
||||
await db.ExecuteAsync("INSERT INTO \"__EFMigrationsHistory\" VALUES (@migration, '8.0.0')", notAppliedMigrations.Select(m => new { migration = m }).ToArray());
|
||||
}
|
||||
await ctx.Database.MigrateAsync();
|
||||
}
|
||||
|
||||
public async Task ContinueMigration()
|
||||
{
|
||||
if (notAppliedMigrations is null)
|
||||
throw new InvalidOperationException("Call MigrateUpTo first");
|
||||
using var ctx = CreateContext();
|
||||
var db = ctx.Database.GetDbConnection();
|
||||
await db.ExecuteAsync("DELETE FROM \"__EFMigrationsHistory\" WHERE \"MigrationId\" = ANY (@migrations)", new { migrations = notAppliedMigrations });
|
||||
await ctx.Database.MigrateAsync();
|
||||
notAppliedMigrations = null;
|
||||
}
|
||||
|
||||
public string ConnectionString { get; }
|
||||
}
|
||||
}
|
217
BTCPayServer.Tests/DatabaseTests.cs
Normal file
217
BTCPayServer.Tests/DatabaseTests.cs
Normal file
@@ -0,0 +1,217 @@
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Payments;
|
||||
using Dapper;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NBitcoin;
|
||||
using NBitcoin.Altcoins;
|
||||
using NBitpayClient;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
[Trait("Integration", "Integration")]
|
||||
public class DatabaseTests : UnitTestBase
|
||||
{
|
||||
|
||||
public DatabaseTests(ITestOutputHelper helper):base(helper)
|
||||
{
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CanQueryMonitoredInvoices()
|
||||
{
|
||||
var tester = CreateDBTester();
|
||||
await tester.MigrateUntil();
|
||||
var invoiceRepository = tester.GetInvoiceRepository();
|
||||
using var ctx = tester.CreateContext();
|
||||
var conn = ctx.Database.GetDbConnection();
|
||||
|
||||
async Task AddPrompt(string invoiceId, string paymentMethodId, bool activated = true)
|
||||
{
|
||||
JObject prompt = new JObject();
|
||||
if (!activated)
|
||||
prompt["inactive"] = true;
|
||||
prompt["currency"] = "USD";
|
||||
var query = """
|
||||
UPDATE "Invoices" SET "Blob2" = jsonb_set('{"prompts": {}}'::JSONB || COALESCE("Blob2",'{}'), ARRAY['prompts','@paymentMethodId'], '@prompt'::JSONB)
|
||||
WHERE "Id" = '@invoiceId'
|
||||
""";
|
||||
query = query.Replace("@paymentMethodId", paymentMethodId);
|
||||
query = query.Replace("@prompt", prompt.ToString());
|
||||
query = query.Replace("@invoiceId", invoiceId);
|
||||
Assert.Equal(1, await conn.ExecuteAsync(query));
|
||||
}
|
||||
|
||||
await conn.ExecuteAsync("""
|
||||
INSERT INTO "Invoices" ("Id", "Created", "Status","Currency") VALUES
|
||||
('BTCOnly', NOW(), 'New', 'USD'),
|
||||
('LTCOnly', NOW(), 'New', 'USD'),
|
||||
('LTCAndBTC', NOW(), 'New', 'USD'),
|
||||
('LTCAndBTCLazy', NOW(), 'New', 'USD')
|
||||
""");
|
||||
foreach (var invoiceId in new string[] { "LTCOnly", "LTCAndBTCLazy", "LTCAndBTC" })
|
||||
{
|
||||
await AddPrompt(invoiceId, "LTC-CHAIN", true);
|
||||
}
|
||||
foreach (var invoiceId in new string[] { "BTCOnly", "LTCAndBTC" })
|
||||
{
|
||||
await AddPrompt(invoiceId, "BTC-CHAIN", true);
|
||||
}
|
||||
await AddPrompt("LTCAndBTCLazy", "BTC-CHAIN", false);
|
||||
|
||||
var btc = PaymentMethodId.Parse("BTC-CHAIN");
|
||||
var ltc = PaymentMethodId.Parse("LTC-CHAIN");
|
||||
var invoices = await invoiceRepository.GetMonitoredInvoices(btc);
|
||||
Assert.Equal(2, invoices.Length);
|
||||
foreach (var invoiceId in new[] { "BTCOnly", "LTCAndBTC" })
|
||||
{
|
||||
Assert.Contains(invoices, i => i.Id == invoiceId);
|
||||
}
|
||||
invoices = await invoiceRepository.GetMonitoredInvoices(btc, true);
|
||||
Assert.Equal(3, invoices.Length);
|
||||
foreach (var invoiceId in new[] { "BTCOnly", "LTCAndBTC", "LTCAndBTCLazy" })
|
||||
{
|
||||
Assert.Contains(invoices, i => i.Id == invoiceId);
|
||||
}
|
||||
|
||||
invoices = await invoiceRepository.GetMonitoredInvoices(ltc);
|
||||
Assert.Equal(3, invoices.Length);
|
||||
foreach (var invoiceId in new[] { "LTCAndBTC", "LTCAndBTC", "LTCAndBTCLazy" })
|
||||
{
|
||||
Assert.Contains(invoices, i => i.Id == invoiceId);
|
||||
}
|
||||
|
||||
await conn.ExecuteAsync("""
|
||||
INSERT INTO "Payments" ("Id", "InvoiceDataId", "PaymentMethodId", "Status", "Blob2", "Created", "Amount", "Currency") VALUES
|
||||
('1','LTCAndBTC', 'LTC-CHAIN', 'Processing', '{}'::JSONB, NOW(), 123, 'USD'),
|
||||
('2','LTCAndBTC', 'BTC-CHAIN', 'Processing', '{}'::JSONB, NOW(), 123, 'USD'),
|
||||
('3','LTCAndBTC', 'BTC-CHAIN', 'Processing', '{}'::JSONB, NOW(), 123, 'USD'),
|
||||
('4','LTCAndBTC', 'BTC-CHAIN', 'Settled', '{}'::JSONB, NOW(), 123, 'USD');
|
||||
|
||||
INSERT INTO "AddressInvoices" ("InvoiceDataId", "Address", "PaymentMethodId") VALUES
|
||||
('LTCAndBTC', 'BTC1', 'BTC-CHAIN'),
|
||||
('LTCAndBTC', 'BTC2', 'BTC-CHAIN'),
|
||||
('LTCAndBTC', 'LTC1', 'LTC-CHAIN');
|
||||
""");
|
||||
|
||||
var invoice = Assert.Single(await invoiceRepository.GetMonitoredInvoices(ltc), i => i.Id == "LTCAndBTC");
|
||||
var payment = Assert.Single(invoice.GetPayments(false));
|
||||
Assert.Equal("1", payment.Id);
|
||||
|
||||
foreach (var includeNonActivated in new[] { true, false })
|
||||
{
|
||||
invoices = await invoiceRepository.GetMonitoredInvoices(btc, includeNonActivated);
|
||||
invoice = Assert.Single(invoices, i => i.Id == "LTCAndBTC");
|
||||
var payments = invoice.GetPayments(false);
|
||||
Assert.Equal(3, payments.Count);
|
||||
|
||||
foreach (var paymentId in new[] { "2", "3", "4" })
|
||||
{
|
||||
Assert.Contains(payments, p => p.Id == paymentId);
|
||||
}
|
||||
Assert.Equal(2, invoice.Addresses.Count);
|
||||
foreach (var addr in new[] { "BTC1", "BTC2" })
|
||||
{
|
||||
Assert.Contains(invoice.Addresses, p => p.Address == addr);
|
||||
}
|
||||
if (!includeNonActivated)
|
||||
Assert.DoesNotContain(invoices, i => i.Id == "LTCAndBTCLazy");
|
||||
else
|
||||
Assert.Contains(invoices, i => i.Id == "LTCAndBTCLazy");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CanMigrateInvoiceAddresses()
|
||||
{
|
||||
var tester = CreateDBTester();
|
||||
await tester.MigrateUntil("20240919085726_refactorinvoiceaddress");
|
||||
using var ctx = tester.CreateContext();
|
||||
var conn = ctx.Database.GetDbConnection();
|
||||
await conn.ExecuteAsync("INSERT INTO \"Invoices\" (\"Id\", \"Created\") VALUES ('i', NOW())");
|
||||
await conn.ExecuteAsync(
|
||||
"INSERT INTO \"AddressInvoices\" VALUES ('aaa#BTC', 'i'),('bbb','i'),('ccc#BTC_LNU', 'i'),('ddd#XMR_MoneroLike', 'i'),('eee#ZEC_ZcashLike', 'i')");
|
||||
await tester.ContinueMigration();
|
||||
foreach (var v in new[] { ("aaa", "BTC-CHAIN"), ("bbb", "BTC-CHAIN"), ("ddd", "XMR-CHAIN") , ("eee", "ZEC-CHAIN") })
|
||||
{
|
||||
var ok = await conn.ExecuteScalarAsync<bool>("SELECT 't'::BOOLEAN FROM \"AddressInvoices\" WHERE \"Address\"=@a AND \"PaymentMethodId\"=@b", new { a = v.Item1, b = v.Item2 });
|
||||
Assert.True(ok);
|
||||
}
|
||||
var notok = await conn.ExecuteScalarAsync<bool>("SELECT 't'::BOOLEAN FROM \"AddressInvoices\" WHERE \"Address\"='ccc'");
|
||||
Assert.False(notok);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CanMigratePayoutsAndPullPayments()
|
||||
{
|
||||
var tester = CreateDBTester();
|
||||
await tester.MigrateUntil("20240827034505_migratepayouts");
|
||||
|
||||
using var ctx = tester.CreateContext();
|
||||
var conn = ctx.Database.GetDbConnection();
|
||||
await conn.ExecuteAsync("INSERT INTO \"Stores\"(\"Id\", \"SpeedPolicy\") VALUES (@store, 0)", new { store = "store1" });
|
||||
var param = new
|
||||
{
|
||||
Id = "pp1",
|
||||
StoreId = "store1",
|
||||
Blob = "{\"Name\": \"CoinLottery\", \"View\": {\"Email\": null, \"Title\": \"\", \"Description\": \"\", \"EmbeddedCSS\": null, \"CustomCSSLink\": null}, \"Limit\": \"10.00\", \"Period\": null, \"Currency\": \"GBP\", \"Description\": \"\", \"Divisibility\": 0, \"MinimumClaim\": \"0\", \"AutoApproveClaims\": false, \"SupportedPaymentMethods\": [\"BTC\", \"BTC_LightningLike\"]}"
|
||||
};
|
||||
await conn.ExecuteAsync("INSERT INTO \"PullPayments\"(\"Id\", \"StoreId\", \"Blob\", \"StartDate\", \"Archived\") VALUES (@Id, @StoreId, @Blob::JSONB, NOW(), 'f')", param);
|
||||
var parameters = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
Id = "p1",
|
||||
StoreId = "store1",
|
||||
PullPaymentDataId = "pp1",
|
||||
PaymentMethodId = "BTC",
|
||||
Blob = "{\"Amount\": \"10.0\", \"Revision\": 0, \"Destination\": \"address\", \"CryptoAmount\": \"0.00012225\", \"MinimumConfirmation\": 1}"
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = "p2",
|
||||
StoreId = "store1",
|
||||
PullPaymentDataId = "pp1",
|
||||
PaymentMethodId = "BTC_LightningLike",
|
||||
Blob = "{\"Amount\": \"10.0\", \"Revision\": 0, \"Destination\": \"address\", \"CryptoAmount\": null, \"MinimumConfirmation\": 1}"
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = "p3",
|
||||
StoreId = "store1",
|
||||
PullPaymentDataId = null as string,
|
||||
PaymentMethodId = "BTC_LightningLike",
|
||||
Blob = "{\"Amount\": \"10.0\", \"Revision\": 0, \"Destination\": \"address\", \"CryptoAmount\": null, \"MinimumConfirmation\": 1}"
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = "p4",
|
||||
StoreId = "store1",
|
||||
PullPaymentDataId = null as string,
|
||||
PaymentMethodId = "BTC_LightningLike",
|
||||
Blob = "{\"Amount\": \"-10.0\", \"Revision\": 0, \"Destination\": \"address\", \"CryptoAmount\": null, \"MinimumConfirmation\": 1}"
|
||||
}
|
||||
};
|
||||
await conn.ExecuteAsync("INSERT INTO \"Payouts\"(\"Id\", \"StoreDataId\", \"PullPaymentDataId\", \"PaymentMethodId\", \"Blob\", \"State\", \"Date\") VALUES (@Id, @StoreId, @PullPaymentDataId, @PaymentMethodId, @Blob::JSONB, 'state', NOW())", parameters);
|
||||
await tester.ContinueMigration();
|
||||
|
||||
var migrated = await conn.ExecuteScalarAsync<bool>("SELECT 't'::BOOLEAN FROM \"PullPayments\" WHERE \"Id\"='pp1' AND \"Limit\"=10.0 AND \"Currency\"='GBP' AND \"Blob\"->>'SupportedPayoutMethods'='[\"BTC-CHAIN\", \"BTC-LN\"]'");
|
||||
Assert.True(migrated);
|
||||
|
||||
migrated = await conn.ExecuteScalarAsync<bool>("SELECT 't'::BOOLEAN FROM \"Payouts\" WHERE \"Id\"='p1' AND \"Amount\"= 0.00012225 AND \"OriginalAmount\"=10.0 AND \"OriginalCurrency\"='GBP' AND \"PayoutMethodId\"='BTC-CHAIN'");
|
||||
Assert.True(migrated);
|
||||
|
||||
migrated = await conn.ExecuteScalarAsync<bool>("SELECT 't'::BOOLEAN FROM \"Payouts\" WHERE \"Id\"='p2' AND \"Amount\" IS NULL AND \"OriginalAmount\"=10.0 AND \"OriginalCurrency\"='GBP' AND \"PayoutMethodId\"='BTC-LN'");
|
||||
Assert.True(migrated);
|
||||
|
||||
migrated = await conn.ExecuteScalarAsync<bool>("SELECT 't'::BOOLEAN FROM \"Payouts\" WHERE \"Id\"='p3' AND \"Amount\" IS NULL AND \"OriginalAmount\"=10.0 AND \"OriginalCurrency\"='BTC'");
|
||||
Assert.True(migrated);
|
||||
|
||||
migrated = await conn.ExecuteScalarAsync<bool>("SELECT 't'::BOOLEAN FROM \"Payouts\" WHERE \"Id\"='p4' AND \"Amount\" IS NULL AND \"OriginalAmount\"=-10.0 AND \"OriginalCurrency\"='BTC' AND \"PayoutMethodId\"='TOPUP'");
|
||||
Assert.True(migrated);
|
||||
}
|
||||
}
|
||||
}
|
@@ -3,14 +3,15 @@ using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection.Metadata;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Security;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Converters;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Configuration;
|
||||
@@ -19,21 +20,31 @@ using BTCPayServer.Data;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Hosting;
|
||||
using BTCPayServer.JsonConverters;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Bitcoin;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Plugins;
|
||||
using BTCPayServer.Plugins.Bitcoin;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Fees;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Labels;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Validation;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Configuration.Memory;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using NBitcoin.Scripting.Parser;
|
||||
using NBXplorer;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using NBXplorer.Models;
|
||||
using Newtonsoft.Json;
|
||||
@@ -41,6 +52,7 @@ using Newtonsoft.Json.Linq;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
using StoreData = BTCPayServer.Data.StoreData;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
@@ -239,12 +251,10 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal(id, id1);
|
||||
Assert.Equal(id, id2);
|
||||
Assert.Equal("LTC-LN", id.ToString());
|
||||
#if ALTCOINS
|
||||
id = PaymentMethodId.Parse("XMR");
|
||||
id1 = PaymentMethodId.Parse("XMR-MoneroLike");
|
||||
Assert.Equal(id, id1);
|
||||
Assert.Equal("XMR-CHAIN", id.ToString());
|
||||
#endif
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -476,7 +486,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
#if ALTCOINS
|
||||
|
||||
[Fact]
|
||||
public void CanCalculateCryptoDue()
|
||||
{
|
||||
@@ -496,7 +506,6 @@ namespace BTCPayServer.Tests
|
||||
|
||||
var paymentMethod = entity.GetPaymentPrompts().TryGet(PaymentTypes.CHAIN.GetPaymentMethodId("BTC"));
|
||||
var accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(1.0m, accounting.ToSmallestUnit(Money.Satoshis(1.0m).ToDecimal(MoneyUnit.BTC)));
|
||||
Assert.Equal(1.1m, accounting.Due);
|
||||
Assert.Equal(1.1m, accounting.TotalDue);
|
||||
|
||||
@@ -647,7 +656,6 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal(accounting.Paid, accounting.TotalDue);
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
#endif
|
||||
|
||||
[Fact]
|
||||
public void DeterministicUTXOSorter()
|
||||
@@ -1617,7 +1625,7 @@ bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku
|
||||
|
||||
testCases.ForEach(tuple =>
|
||||
{
|
||||
Assert.Equal(tuple.expectedOutput, PosDataParser.ParsePosData(string.IsNullOrEmpty(tuple.input) ? null : JToken.Parse(tuple.input)));
|
||||
Assert.Equal(tuple.expectedOutput, UIInvoiceController.PosDataParser.ParsePosData(string.IsNullOrEmpty(tuple.input) ? null : JToken.Parse(tuple.input)));
|
||||
});
|
||||
}
|
||||
[Fact]
|
||||
@@ -2238,7 +2246,7 @@ bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku
|
||||
Data.InvoiceData data = new Data.InvoiceData();
|
||||
obj = data;
|
||||
data.Blob2 = v.Input.ToString();
|
||||
data.Migrate();
|
||||
data.TryMigrate();
|
||||
var actual = JObject.Parse(data.Blob2);
|
||||
AssertSameJson(v.Expected, actual);
|
||||
if (!v.SkipRountripTest)
|
||||
@@ -2258,7 +2266,7 @@ bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku
|
||||
//data.
|
||||
obj = data;
|
||||
data.Blob2 = v.Input.ToString();
|
||||
data.Migrate();
|
||||
data.TryMigrate();
|
||||
var actual = JObject.Parse(data.Blob2);
|
||||
AssertSameJson(v.Expected, actual);
|
||||
if (!v.SkipRountripTest)
|
||||
@@ -2373,7 +2381,7 @@ bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku
|
||||
});
|
||||
var data = new Data.InvoiceData();
|
||||
data.Blob2 = o.ToString();
|
||||
data.Migrate();
|
||||
data.TryMigrate();
|
||||
var migrated = JObject.Parse(data.Blob2);
|
||||
return migrated["prompts"]["BTC-CHAIN"]["details"]["accountDerivation"].Value<string>();
|
||||
})
|
||||
|
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -13,9 +14,14 @@ using BTCPayServer.Events;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Models.InvoicingModels;
|
||||
using BTCPayServer.NTag424;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.PayoutProcessors;
|
||||
using BTCPayServer.Plugins.PointOfSale.Controllers;
|
||||
using BTCPayServer.Plugins.PointOfSale.Models;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Notifications;
|
||||
using BTCPayServer.Services.Notifications.Blobs;
|
||||
using BTCPayServer.Services.Stores;
|
||||
@@ -30,6 +36,7 @@ using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
using Xunit.Sdk;
|
||||
using CreateApplicationUserRequest = BTCPayServer.Client.Models.CreateApplicationUserRequest;
|
||||
using PosViewType = BTCPayServer.Plugins.PointOfSale.PosViewType;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
@@ -678,6 +685,93 @@ namespace BTCPayServer.Tests
|
||||
Assert.False(apps[2].Archived);
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanGetAppStats()
|
||||
{
|
||||
using var tester = CreateServerTester();
|
||||
await tester.StartAsync();
|
||||
var user = tester.NewAccount();
|
||||
await user.GrantAccessAsync(true);
|
||||
await user.MakeAdmin();
|
||||
await user.RegisterDerivationSchemeAsync("BTC");
|
||||
var client = await user.CreateClient();
|
||||
|
||||
var item1 = new ViewPointOfSaleViewModel.Item { Id = "item1", Title = "Item 1", Price = 1, PriceType = ViewPointOfSaleViewModel.ItemPriceType.Fixed };
|
||||
var item2 = new ViewPointOfSaleViewModel.Item { Id = "item2", Title = "Item 2", Price = 2, PriceType = ViewPointOfSaleViewModel.ItemPriceType.Fixed };
|
||||
var item3 = new ViewPointOfSaleViewModel.Item { Id = "item3", Title = "Item 3", Price = 3, PriceType = ViewPointOfSaleViewModel.ItemPriceType.Fixed };
|
||||
var posItems = AppService.SerializeTemplate([item1, item2, item3]);
|
||||
var posApp = await client.CreatePointOfSaleApp(user.StoreId, new PointOfSaleAppRequest { AppName = "test pos", Template = posItems, });
|
||||
var crowdfundApp = await client.CreateCrowdfundApp(user.StoreId, new CrowdfundAppRequest { AppName = "test crowdfund" });
|
||||
|
||||
// empty states
|
||||
var posSales = await client.GetAppSales(posApp.Id);
|
||||
Assert.NotNull(posSales);
|
||||
Assert.Equal(0, posSales.SalesCount);
|
||||
Assert.Equal(7, posSales.Series.Count());
|
||||
|
||||
var crowdfundSales = await client.GetAppSales(crowdfundApp.Id);
|
||||
Assert.NotNull(crowdfundSales);
|
||||
Assert.Equal(0, crowdfundSales.SalesCount);
|
||||
Assert.Equal(7, crowdfundSales.Series.Count());
|
||||
|
||||
var posTopItems = await client.GetAppTopItems(posApp.Id);
|
||||
Assert.Empty(posTopItems);
|
||||
|
||||
var crowdfundItems = await client.GetAppTopItems(crowdfundApp.Id);
|
||||
Assert.Empty(crowdfundItems);
|
||||
|
||||
// with sales - fiddle invoices via the UI controller
|
||||
var uiPosController = tester.PayTester.GetController<UIPointOfSaleController>();
|
||||
|
||||
var action = Assert.IsType<RedirectToActionResult>(uiPosController.ViewPointOfSale(posApp.Id, PosViewType.Static, 1, choiceKey: item1.Id).GetAwaiter().GetResult());
|
||||
Assert.Equal(nameof(UIInvoiceController.Checkout), action.ActionName);
|
||||
Assert.True(action.RouteValues!.TryGetValue("invoiceId", out var i1Id));
|
||||
|
||||
var cart = new JObject {
|
||||
["cart"] = new JArray
|
||||
{
|
||||
new JObject { ["id"] = item2.Id, ["count"] = 4 },
|
||||
new JObject { ["id"] = item3.Id, ["count"] = 2 }
|
||||
},
|
||||
["subTotal"] = 14,
|
||||
["total"] = 14,
|
||||
["amounts"] = new JArray()
|
||||
}.ToString();
|
||||
action = Assert.IsType<RedirectToActionResult>(uiPosController.ViewPointOfSale(posApp.Id, PosViewType.Cart, 7, posData: cart).GetAwaiter().GetResult());
|
||||
Assert.Equal(nameof(UIInvoiceController.Checkout), action.ActionName);
|
||||
Assert.True(action.RouteValues!.TryGetValue("invoiceId", out var i2Id));
|
||||
|
||||
await user.PayInvoice(i1Id!.ToString());
|
||||
await user.PayInvoice(i2Id!.ToString());
|
||||
|
||||
posSales = await client.GetAppSales(posApp.Id);
|
||||
Assert.Equal(7, posSales.SalesCount);
|
||||
Assert.Equal(7, posSales.Series.Count());
|
||||
Assert.Equal(0, posSales.Series.First().SalesCount);
|
||||
Assert.Equal(7, posSales.Series.Last().SalesCount);
|
||||
|
||||
posTopItems = await client.GetAppTopItems(posApp.Id);
|
||||
Assert.Equal(3, posTopItems.Count);
|
||||
Assert.Equal(item2.Id, posTopItems[0].ItemCode);
|
||||
Assert.Equal(4, posTopItems[0].SalesCount);
|
||||
|
||||
Assert.Equal(item3.Id, posTopItems[1].ItemCode);
|
||||
Assert.Equal(2, posTopItems[1].SalesCount);
|
||||
|
||||
Assert.Equal(item1.Id, posTopItems[2].ItemCode);
|
||||
Assert.Equal(1, posTopItems[2].SalesCount);
|
||||
|
||||
// with count and offset
|
||||
posTopItems = await client.GetAppTopItems(posApp.Id,1, 5);
|
||||
Assert.Equal(2, posTopItems.Count);
|
||||
Assert.Equal(item3.Id, posTopItems[0].ItemCode);
|
||||
Assert.Equal(2, posTopItems[0].SalesCount);
|
||||
|
||||
Assert.Equal(item1.Id, posTopItems[1].ItemCode);
|
||||
Assert.Equal(1, posTopItems[1].SalesCount);
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanDeleteUsersViaApi()
|
||||
@@ -1071,19 +1165,19 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
Destination = destination,
|
||||
Amount = 1_000_000m,
|
||||
PaymentMethod = "BTC",
|
||||
PayoutMethodId = "BTC",
|
||||
}));
|
||||
|
||||
await this.AssertAPIError("archived", async () => await unauthenticated.CreatePayout(pps[1].Id, new CreatePayoutRequest()
|
||||
{
|
||||
Destination = destination,
|
||||
PaymentMethod = "BTC"
|
||||
PayoutMethodId = "BTC"
|
||||
}));
|
||||
|
||||
var payout = await unauthenticated.CreatePayout(pps[0].Id, new CreatePayoutRequest()
|
||||
{
|
||||
Destination = destination,
|
||||
PaymentMethod = "BTC"
|
||||
PayoutMethodId = "BTC"
|
||||
});
|
||||
|
||||
payouts = await unauthenticated.GetPayouts(pps[0].Id);
|
||||
@@ -1092,7 +1186,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal(payout.Id, payout2.Id);
|
||||
Assert.Equal(destination, payout2.Destination);
|
||||
Assert.Equal(PayoutState.AwaitingApproval, payout.State);
|
||||
Assert.Equal("BTC-CHAIN", payout2.PaymentMethod);
|
||||
Assert.Equal("BTC-CHAIN", payout2.PayoutMethodId);
|
||||
Assert.Equal("BTC", payout2.CryptoCode);
|
||||
Assert.Null(payout.PaymentMethodAmount);
|
||||
|
||||
@@ -1103,14 +1197,14 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
Destination = destination2,
|
||||
Amount = 0.00001m,
|
||||
PaymentMethod = "BTC"
|
||||
PayoutMethodId = "BTC"
|
||||
}));
|
||||
|
||||
TestLogs.LogInformation("Can't create too low payout");
|
||||
await this.AssertAPIError("amount-too-low", async () => await unauthenticated.CreatePayout(pps[0].Id, new CreatePayoutRequest()
|
||||
{
|
||||
Destination = destination2,
|
||||
PaymentMethod = "BTC"
|
||||
PayoutMethodId = "BTC"
|
||||
}));
|
||||
|
||||
TestLogs.LogInformation("Can archive payout");
|
||||
@@ -1126,7 +1220,7 @@ namespace BTCPayServer.Tests
|
||||
payout = await unauthenticated.CreatePayout(pps[0].Id, new CreatePayoutRequest()
|
||||
{
|
||||
Destination = destination,
|
||||
PaymentMethod = "BTC"
|
||||
PayoutMethodId = "BTC"
|
||||
});
|
||||
|
||||
var start = RoundSeconds(DateTimeOffset.Now + TimeSpan.FromDays(7.0));
|
||||
@@ -1144,7 +1238,7 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
Amount = 1.0m,
|
||||
Destination = destination,
|
||||
PaymentMethod = "BTC"
|
||||
PayoutMethodId = "BTC"
|
||||
}));
|
||||
|
||||
var expires = RoundSeconds(DateTimeOffset.Now - TimeSpan.FromDays(7.0));
|
||||
@@ -1160,7 +1254,7 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
Amount = 1.0m,
|
||||
Destination = destination,
|
||||
PaymentMethod = "BTC"
|
||||
PayoutMethodId = "BTC"
|
||||
}));
|
||||
|
||||
await this.AssertValidationError(new[] { "ExpiresAt" }, async () => await client.CreatePullPayment(storeId, new Client.Models.CreatePullPaymentRequest()
|
||||
@@ -1188,7 +1282,7 @@ namespace BTCPayServer.Tests
|
||||
payout = await unauthenticated.CreatePayout(pp.Id, new CreatePayoutRequest()
|
||||
{
|
||||
Destination = destination,
|
||||
PaymentMethod = "BTC"
|
||||
PayoutMethodId = "BTC"
|
||||
});
|
||||
await this.AssertAPIError("old-revision", async () => await client.ApprovePayout(storeId, payout.Id, new ApprovePayoutRequest()
|
||||
{
|
||||
@@ -1222,7 +1316,7 @@ namespace BTCPayServer.Tests
|
||||
payout = await unauthenticated.CreatePayout(test3.Id, new CreatePayoutRequest()
|
||||
{
|
||||
Destination = destination,
|
||||
PaymentMethod = "BTC"
|
||||
PayoutMethodId = "BTC"
|
||||
});
|
||||
payout = await client.ApprovePayout(storeId, payout.Id, new ApprovePayoutRequest());
|
||||
// The payout should round the value of the payment down to the network of the payment method
|
||||
@@ -1342,7 +1436,7 @@ namespace BTCPayServer.Tests
|
||||
await nonApproved.CreatePayout(acc.StoreId, new CreatePayoutThroughStoreRequest()
|
||||
{
|
||||
Amount = 100,
|
||||
PaymentMethod = "BTC",
|
||||
PayoutMethodId = "BTC",
|
||||
Approved = true,
|
||||
Destination = new Key().GetAddress(ScriptPubKeyType.TaprootBIP86, Network.RegTest).ToString()
|
||||
});
|
||||
@@ -1360,7 +1454,7 @@ namespace BTCPayServer.Tests
|
||||
await approved.CreatePayout(acc.StoreId, new CreatePayoutThroughStoreRequest()
|
||||
{
|
||||
Amount = 100,
|
||||
PaymentMethod = "BTC",
|
||||
PayoutMethodId = "BTC",
|
||||
Approved = true,
|
||||
Destination = new Key().GetAddress(ScriptPubKeyType.TaprootBIP86, Network.RegTest).ToString()
|
||||
});
|
||||
@@ -1381,7 +1475,7 @@ namespace BTCPayServer.Tests
|
||||
var payout = await client.CreatePayout(storeId, new CreatePayoutThroughStoreRequest()
|
||||
{
|
||||
Approved = false,
|
||||
PaymentMethod = "BTC",
|
||||
PayoutMethodId = "BTC",
|
||||
Amount = 0.0001m,
|
||||
Destination = address.ToString(),
|
||||
|
||||
@@ -1408,7 +1502,7 @@ namespace BTCPayServer.Tests
|
||||
payout = await client.CreatePayout(storeId, new CreatePayoutThroughStoreRequest()
|
||||
{
|
||||
Approved = true,
|
||||
PaymentMethod = "BTC",
|
||||
PayoutMethodId = "BTC",
|
||||
Amount = 0.0001m,
|
||||
Destination = address.ToString()
|
||||
});
|
||||
@@ -1529,13 +1623,14 @@ namespace BTCPayServer.Tests
|
||||
CssUrl = "https://example.org/style.css",
|
||||
LogoUrl = "https://example.org/logo.svg",
|
||||
BrandColor = "#003366",
|
||||
ApplyBrandColorToBackend = true,
|
||||
PaymentMethodCriteria = new List<PaymentMethodCriteriaData>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Amount = 10,
|
||||
Above = true,
|
||||
PaymentMethod = "BTC",
|
||||
PaymentMethodId = "BTC",
|
||||
CurrencyCode = "USD"
|
||||
}
|
||||
}
|
||||
@@ -1544,13 +1639,14 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal("https://example.org/style.css", updatedStore.CssUrl);
|
||||
Assert.Equal("https://example.org/logo.svg", updatedStore.LogoUrl);
|
||||
Assert.Equal("#003366", updatedStore.BrandColor);
|
||||
Assert.True(updatedStore.ApplyBrandColorToBackend);
|
||||
var s = (await client.GetStore(newStore.Id));
|
||||
Assert.Equal("B", s.Name);
|
||||
var pmc = Assert.Single(s.PaymentMethodCriteria);
|
||||
//check that pmc equals the one we set
|
||||
Assert.Equal(10, pmc.Amount);
|
||||
Assert.True(pmc.Above);
|
||||
Assert.Equal("BTC-CHAIN", pmc.PaymentMethod);
|
||||
Assert.Equal("BTC-CHAIN", pmc.PaymentMethodId);
|
||||
Assert.Equal("USD", pmc.CurrencyCode);
|
||||
updatedStore = await client.UpdateStore(newStore.Id, new UpdateStoreRequest() { Name = "B" });
|
||||
Assert.Empty(newStore.PaymentMethodCriteria);
|
||||
@@ -1711,7 +1807,7 @@ namespace BTCPayServer.Tests
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
var clientProfile = await user.CreateClient(Policies.CanModifyStoreWebhooks, Policies.CanCreateInvoice);
|
||||
var clientProfile = await user.CreateClient(Policies.CanModifyWebhooks, Policies.CanCreateInvoice);
|
||||
var hook = await clientProfile.CreateWebhook(user.StoreId, new CreateStoreWebhookRequest()
|
||||
{
|
||||
Url = fakeServer.ServerUri.AbsoluteUri,
|
||||
@@ -2138,7 +2234,7 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
await client.RefundInvoice(user.StoreId, "lol fake invoice id", new RefundInvoiceRequest()
|
||||
{
|
||||
PaymentMethod = method.PaymentMethodId,
|
||||
PayoutMethodId = method.PaymentMethodId,
|
||||
RefundVariant = RefundVariant.RateThen
|
||||
});
|
||||
});
|
||||
@@ -2146,7 +2242,7 @@ namespace BTCPayServer.Tests
|
||||
// test validation error for when invoice is not yet in the state in which it can be refunded
|
||||
var apiError = await AssertAPIError("non-refundable", () => client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest()
|
||||
{
|
||||
PaymentMethod = method.PaymentMethodId,
|
||||
PayoutMethodId = method.PaymentMethodId,
|
||||
RefundVariant = RefundVariant.RateThen
|
||||
}));
|
||||
Assert.Equal("Cannot refund this invoice", apiError.Message);
|
||||
@@ -2164,20 +2260,20 @@ namespace BTCPayServer.Tests
|
||||
});
|
||||
|
||||
// test validation for the payment method
|
||||
var validationError = await AssertValidationError(new[] { "PaymentMethod" }, async () =>
|
||||
var validationError = await AssertValidationError(new[] { "PayoutMethodId" }, async () =>
|
||||
{
|
||||
await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest()
|
||||
{
|
||||
PaymentMethod = "fake payment method",
|
||||
PayoutMethodId = "fake payment method",
|
||||
RefundVariant = RefundVariant.RateThen
|
||||
});
|
||||
});
|
||||
Assert.Contains("PaymentMethod: Please select one of the payment methods which were available for the original invoice", validationError.Message);
|
||||
Assert.Contains("PayoutMethodId: Please select one of the payment methods which were available for the original invoice", validationError.Message);
|
||||
|
||||
// test RefundVariant.RateThen
|
||||
var pp = await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest()
|
||||
{
|
||||
PaymentMethod = method.PaymentMethodId,
|
||||
PayoutMethodId = method.PaymentMethodId,
|
||||
RefundVariant = RefundVariant.RateThen
|
||||
});
|
||||
Assert.Equal("BTC", pp.Currency);
|
||||
@@ -2188,7 +2284,7 @@ namespace BTCPayServer.Tests
|
||||
// test RefundVariant.CurrentRate
|
||||
pp = await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest()
|
||||
{
|
||||
PaymentMethod = method.PaymentMethodId,
|
||||
PayoutMethodId = method.PaymentMethodId,
|
||||
RefundVariant = RefundVariant.CurrentRate
|
||||
});
|
||||
Assert.Equal("BTC", pp.Currency);
|
||||
@@ -2198,7 +2294,7 @@ namespace BTCPayServer.Tests
|
||||
// test RefundVariant.Fiat
|
||||
pp = await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest()
|
||||
{
|
||||
PaymentMethod = method.PaymentMethodId,
|
||||
PayoutMethodId = method.PaymentMethodId,
|
||||
RefundVariant = RefundVariant.Fiat,
|
||||
Name = "my test name"
|
||||
});
|
||||
@@ -2212,7 +2308,7 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest()
|
||||
{
|
||||
PaymentMethod = method.PaymentMethodId,
|
||||
PayoutMethodId = method.PaymentMethodId,
|
||||
RefundVariant = RefundVariant.Custom,
|
||||
});
|
||||
});
|
||||
@@ -2221,7 +2317,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
pp = await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest()
|
||||
{
|
||||
PaymentMethod = method.PaymentMethodId,
|
||||
PayoutMethodId = method.PaymentMethodId,
|
||||
RefundVariant = RefundVariant.Custom,
|
||||
CustomAmount = 69420,
|
||||
CustomCurrency = "JPY"
|
||||
@@ -2233,7 +2329,7 @@ namespace BTCPayServer.Tests
|
||||
// should auto-approve if currencies match
|
||||
pp = await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest()
|
||||
{
|
||||
PaymentMethod = method.PaymentMethodId,
|
||||
PayoutMethodId = method.PaymentMethodId,
|
||||
RefundVariant = RefundVariant.Custom,
|
||||
CustomAmount = 0.00069420m,
|
||||
CustomCurrency = "BTC"
|
||||
@@ -2245,7 +2341,7 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest
|
||||
{
|
||||
PaymentMethod = method.PaymentMethodId,
|
||||
PayoutMethodId = method.PaymentMethodId,
|
||||
RefundVariant = RefundVariant.RateThen,
|
||||
SubtractPercentage = 101
|
||||
});
|
||||
@@ -2255,7 +2351,7 @@ namespace BTCPayServer.Tests
|
||||
// should auto-approve
|
||||
pp = await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest
|
||||
{
|
||||
PaymentMethod = method.PaymentMethodId,
|
||||
PayoutMethodId = method.PaymentMethodId,
|
||||
RefundVariant = RefundVariant.RateThen,
|
||||
SubtractPercentage = 6.15m
|
||||
});
|
||||
@@ -2268,7 +2364,7 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest
|
||||
{
|
||||
PaymentMethod = method.PaymentMethodId,
|
||||
PayoutMethodId = method.PaymentMethodId,
|
||||
RefundVariant = RefundVariant.OverpaidAmount
|
||||
});
|
||||
});
|
||||
@@ -2298,7 +2394,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
pp = await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest
|
||||
{
|
||||
PaymentMethod = method.PaymentMethodId,
|
||||
PayoutMethodId = method.PaymentMethodId,
|
||||
RefundVariant = RefundVariant.OverpaidAmount
|
||||
});
|
||||
Assert.Equal("BTC", pp.Currency);
|
||||
@@ -2308,7 +2404,7 @@ namespace BTCPayServer.Tests
|
||||
// once more with subtract percentage
|
||||
pp = await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest
|
||||
{
|
||||
PaymentMethod = method.PaymentMethodId,
|
||||
PayoutMethodId = method.PaymentMethodId,
|
||||
RefundVariant = RefundVariant.OverpaidAmount,
|
||||
SubtractPercentage = 21m
|
||||
});
|
||||
@@ -2321,7 +2417,7 @@ namespace BTCPayServer.Tests
|
||||
await client.MarkInvoiceStatus(user.StoreId, invoice.Id, new MarkInvoiceStatusRequest { Status = InvoiceStatus.Settled });
|
||||
var refund = await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest
|
||||
{
|
||||
PaymentMethod = method.PaymentMethodId,
|
||||
PayoutMethodId = method.PaymentMethodId,
|
||||
RefundVariant = RefundVariant.CurrentRate
|
||||
});
|
||||
Assert.Equal(1.0m, refund.Amount);
|
||||
@@ -2616,6 +2712,14 @@ namespace BTCPayServer.Tests
|
||||
invoiceObject = await client.GetOnChainWalletObject(user.StoreId, "BTC", new OnChainWalletObjectId("invoice", invoice.Id), false);
|
||||
Assert.DoesNotContain(invoiceObject.Links.Select(l => l.Type), t => t == "address");
|
||||
|
||||
// Check if we can get the monitored invoice
|
||||
var invoiceRepo = tester.PayTester.GetService<InvoiceRepository>();
|
||||
var includeNonActivated = true;
|
||||
Assert.Single(await invoiceRepo.GetMonitoredInvoices(PaymentMethodId.Parse("BTC-CHAIN"), includeNonActivated), i => i.Id == invoice.Id);
|
||||
includeNonActivated = false;
|
||||
Assert.DoesNotContain(await invoiceRepo.GetMonitoredInvoices(PaymentMethodId.Parse("BTC-CHAIN"), includeNonActivated), i => i.Id == invoice.Id);
|
||||
Assert.DoesNotContain(await invoiceRepo.GetMonitoredInvoices(PaymentMethodId.Parse("BTC-CHAIN")), i => i.Id == invoice.Id);
|
||||
//
|
||||
|
||||
paymentMethods = await client.GetInvoicePaymentMethods(store.Id, invoice.Id);
|
||||
Assert.Single(paymentMethods);
|
||||
@@ -3105,6 +3209,8 @@ namespace BTCPayServer.Tests
|
||||
});
|
||||
|
||||
Assert.Equal(firstAddress, (await viewOnlyClient.PreviewProposedStoreOnChainPaymentMethodAddresses(store.Id, "BTC", xpub)).Addresses.First().Address);
|
||||
// Testing if the rewrite rule to old API path is working
|
||||
await viewOnlyClient.SendHttpRequest($"api/v1/stores/{store.Id}/payment-methods/onchain/BTC/preview", new JObject() { ["config"] = xpub.ToString() }, HttpMethod.Post);
|
||||
|
||||
var method = await client.UpdateStorePaymentMethod(store.Id, "BTC-CHAIN", new UpdatePaymentMethodRequest() { Enabled = true, Config = JValue.CreateString(xpub.ToString())});
|
||||
|
||||
@@ -3158,8 +3264,8 @@ namespace BTCPayServer.Tests
|
||||
|
||||
await client.RemoveStorePaymentMethod(store.Id, "BTC-CHAIN");
|
||||
generateResponse = await client.GenerateOnChainWallet(store.Id, "BTC",
|
||||
new GenerateOnChainWalletRequest() { ExistingMnemonic = allMnemonic, AccountNumber = 1 });
|
||||
|
||||
new GenerateOnChainWalletRequest() { ExistingMnemonic = allMnemonic, AccountNumber = 1, Label = "test" });
|
||||
Assert.Equal("test", generateResponse.Config.Label);
|
||||
Assert.Equal(generateResponse.Mnemonic.ToString(), allMnemonic.ToString());
|
||||
|
||||
Assert.Equal(new Mnemonic("all all all all all all all all all all all all").DeriveExtKey()
|
||||
@@ -3384,6 +3490,12 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
await viewOnlyClient.GetOnChainWalletReceiveAddress(walletId.StoreId, walletId.CryptoCode);
|
||||
});
|
||||
|
||||
// Testing if the rewrite rule to old API path is working
|
||||
await AssertHttpError(403, async () =>
|
||||
{
|
||||
await viewOnlyClient.SendHttpRequest($"api/v1/stores/{walletId.StoreId}/payment-methods/onchain/{walletId.CryptoCode}/wallet/address", null as object);
|
||||
});
|
||||
var address = await client.GetOnChainWalletReceiveAddress(walletId.StoreId, walletId.CryptoCode);
|
||||
var address2 = await client.GetOnChainWalletReceiveAddress(walletId.StoreId, walletId.CryptoCode);
|
||||
var address3 = await client.GetOnChainWalletReceiveAddress(walletId.StoreId, walletId.CryptoCode, true);
|
||||
@@ -4010,7 +4122,12 @@ namespace BTCPayServer.Tests
|
||||
var resp = await tester.CustomerLightningD.Pay(inv.BOLT11);
|
||||
Assert.Equal(PayResult.Ok, resp.Result);
|
||||
|
||||
|
||||
var store = tester.PayTester.GetService<StoreRepository>();
|
||||
Assert.True(await store.InternalNodePayoutAuthorized(admin.StoreId));
|
||||
Assert.False(await store.InternalNodePayoutAuthorized("blah"));
|
||||
await admin.MakeAdmin(false);
|
||||
Assert.False(await store.InternalNodePayoutAuthorized(admin.StoreId));
|
||||
await admin.MakeAdmin(true);
|
||||
|
||||
var customerInvoice = await tester.CustomerLightningD.CreateInvoice(LightMoney.FromUnit(10, LightMoneyUnit.Satoshi),
|
||||
Guid.NewGuid().ToString(), TimeSpan.FromDays(40));
|
||||
@@ -4018,7 +4135,7 @@ namespace BTCPayServer.Tests
|
||||
new CreatePayoutThroughStoreRequest()
|
||||
{
|
||||
Approved = true,
|
||||
PaymentMethod = "BTC_LightningNetwork",
|
||||
PayoutMethodId = "BTC_LightningNetwork",
|
||||
Destination = customerInvoice.BOLT11
|
||||
});
|
||||
Assert.Equal(payout.Metadata.ToString(), new JObject().ToString()); //empty
|
||||
@@ -4037,7 +4154,7 @@ namespace BTCPayServer.Tests
|
||||
new CreatePayoutThroughStoreRequest()
|
||||
{
|
||||
Approved = true,
|
||||
PaymentMethod = "BTC",
|
||||
PayoutMethodId = "BTC",
|
||||
Destination = (await tester.ExplorerNode.GetNewAddressAsync()).ToString(),
|
||||
Amount = 0.0001m,
|
||||
Metadata = JObject.FromObject(new
|
||||
@@ -4068,7 +4185,7 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
Approved = true,
|
||||
Amount = new Money(100, MoneyUnit.Satoshi).ToDecimal(MoneyUnit.BTC),
|
||||
PaymentMethod = "BTC_LightningNetwork",
|
||||
PayoutMethodId = "BTC_LightningNetwork",
|
||||
Destination = customerInvoice.BOLT11
|
||||
});
|
||||
Assert.Equal(payout2.Amount, new Money(100, MoneyUnit.Satoshi).ToDecimal(MoneyUnit.BTC));
|
||||
@@ -4098,7 +4215,7 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
Amount = 0.0001m,
|
||||
Approved = true,
|
||||
PaymentMethod = "BTC",
|
||||
PayoutMethodId = "BTC",
|
||||
Destination = (await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
|
||||
});
|
||||
|
||||
@@ -4106,7 +4223,7 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
Amount = 0.00001m,
|
||||
Approved = false,
|
||||
PaymentMethod = "BTC",
|
||||
PayoutMethodId = "BTC",
|
||||
Destination = (await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
|
||||
});
|
||||
|
||||
@@ -4123,7 +4240,7 @@ namespace BTCPayServer.Tests
|
||||
PullPaymentId = pullPayment.Id,
|
||||
Amount = 10,
|
||||
Approved = false,
|
||||
PaymentMethod = "BTC",
|
||||
PayoutMethodId = "BTC",
|
||||
Destination = (await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
|
||||
});
|
||||
await adminClient.ApprovePayout(admin.StoreId, notapprovedPayoutWithPullPayment.Id,
|
||||
@@ -4254,7 +4371,7 @@ namespace BTCPayServer.Tests
|
||||
PullPaymentId = pullPayment.Id,
|
||||
Amount = 0.5m,
|
||||
Approved = true,
|
||||
PaymentMethod = "BTC",
|
||||
PayoutMethodId = "BTC",
|
||||
Destination = (await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
|
||||
});
|
||||
TestLogs.LogInformation("Waiting before hook...");
|
||||
@@ -4300,7 +4417,7 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
Amount = 0.1m,
|
||||
Approved = true,
|
||||
PaymentMethod = "BTC",
|
||||
PayoutMethodId = "BTC",
|
||||
Destination = (await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
|
||||
});
|
||||
|
||||
@@ -4316,7 +4433,7 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
Amount = 0.3m,
|
||||
Approved = true,
|
||||
PaymentMethod = "BTC",
|
||||
PayoutMethodId = "BTC",
|
||||
Destination = (await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
|
||||
});
|
||||
|
||||
@@ -4333,7 +4450,7 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
Amount = 0.3m,
|
||||
Approved = true,
|
||||
PaymentMethod = "BTC",
|
||||
PayoutMethodId = "BTC",
|
||||
Destination = (await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
|
||||
});
|
||||
|
||||
|
@@ -50,10 +50,8 @@ namespace BTCPayServer.Tests
|
||||
tester.Driver.FindElement(By.Name("Name")).SendKeys("English (Custom)");
|
||||
tester.ClickPagePrimary();
|
||||
var translations = tester.Driver.FindElement(By.Name("Translations"));
|
||||
var text = translations.Text;
|
||||
text = text.Replace("Password => Password", "Password => Mot de passe");
|
||||
translations.Clear();
|
||||
translations.SendKeys("Password => Mot de passe");
|
||||
translations.SendKeys("{ \"Password\": \"Mot de passe\" }");
|
||||
tester.ClickPagePrimary();
|
||||
|
||||
// Check English (Custom) can be selected
|
||||
@@ -64,7 +62,7 @@ namespace BTCPayServer.Tests
|
||||
// Check if we can remove English (Custom)
|
||||
tester.LogIn();
|
||||
tester.GoToServer(Views.Server.ServerNavPages.Translations);
|
||||
text = tester.Driver.PageSource;
|
||||
var text = tester.Driver.PageSource;
|
||||
Assert.Contains("Select-Cypherpunk", text);
|
||||
Assert.DoesNotContain("Select-English (Custom)", text);
|
||||
// Cypherpunk is loaded from file, can't edit
|
||||
|
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
@@ -121,13 +122,6 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.FindElement(By.Id("SignTransaction")).Click();
|
||||
}
|
||||
|
||||
private static string ExtractPSBT(SeleniumTester s)
|
||||
{
|
||||
var pageSource = s.Driver.PageSource;
|
||||
var start = pageSource.IndexOf("id=\"psbt-base64\">");
|
||||
start += "id=\"psbt-base64\">".Length;
|
||||
var end = pageSource.IndexOf("<", start);
|
||||
return pageSource[start..end];
|
||||
}
|
||||
private string ExtractPSBT(SeleniumTester s) => s.Driver.FindElement(By.Id("psbt-base64")).GetAttribute("innerText");
|
||||
}
|
||||
}
|
||||
|
@@ -5,10 +5,8 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Lightning.CLightning;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Views.Manage;
|
||||
using BTCPayServer.Views.Server;
|
||||
using BTCPayServer.Views.Stores;
|
||||
@@ -18,7 +16,6 @@ using NBitcoin;
|
||||
using NBitcoin.RPC;
|
||||
using OpenQA.Selenium;
|
||||
using OpenQA.Selenium.Chrome;
|
||||
using OpenQA.Selenium.Support.Extensions;
|
||||
using OpenQA.Selenium.Support.UI;
|
||||
using Xunit;
|
||||
|
||||
@@ -58,6 +55,7 @@ namespace BTCPayServer.Tests
|
||||
options.AddArguments($"window-size={windowSize.Width}x{windowSize.Height}");
|
||||
options.AddArgument("shm-size=2g");
|
||||
options.AddArgument("start-maximized");
|
||||
options.AddArgument("disable-search-engine-choice-screen");
|
||||
if (Server.PayTester.InContainer)
|
||||
{
|
||||
// Shot in the dark to fix https://stackoverflow.com/questions/53902507/unknown-error-session-deleted-because-of-page-crash-from-unknown-error-cannot
|
||||
@@ -93,7 +91,15 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
if (amount is not null)
|
||||
{
|
||||
Driver.FindElement(By.Id("test-payment-amount")).Clear();
|
||||
try
|
||||
{
|
||||
Driver.FindElement(By.Id("test-payment-amount")).Clear();
|
||||
}
|
||||
// Sometimes the element is not available after a window switch... retry
|
||||
catch (StaleElementReferenceException)
|
||||
{
|
||||
Driver.FindElement(By.Id("test-payment-amount")).Clear();
|
||||
}
|
||||
Driver.FindElement(By.Id("test-payment-amount")).SendKeys(amount.ToString());
|
||||
}
|
||||
Driver.WaitUntilAvailable(By.Id("FakePayment"));
|
||||
@@ -541,7 +547,6 @@ retry:
|
||||
{
|
||||
walletId ??= WalletId;
|
||||
GoToWallet(walletId, WalletsNavPages.Receive);
|
||||
Driver.FindElement(By.Id("generateButton")).Click();
|
||||
var addressStr = Driver.FindElement(By.Id("Address")).GetAttribute("data-text");
|
||||
var address = BitcoinAddress.Create(addressStr, ((BTCPayNetwork)Server.NetworkProvider.GetNetwork(walletId.CryptoCode)).NBitcoinNetwork);
|
||||
for (var i = 0; i < coins; i++)
|
||||
|
@@ -6,7 +6,6 @@ using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
@@ -20,6 +19,7 @@ using BTCPayServer.NTag424;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using BTCPayServer.Views.Manage;
|
||||
using BTCPayServer.Views.Server;
|
||||
@@ -28,7 +28,6 @@ using BTCPayServer.Views.Wallets;
|
||||
using ExchangeSharp;
|
||||
using LNURL;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc.Routing;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
@@ -373,7 +372,7 @@ namespace BTCPayServer.Tests
|
||||
var usr = RandomUtils.GetUInt256().ToString().Substring(64 - 20) + "@a.com";
|
||||
s.Driver.FindElement(By.Id("Email")).SendKeys(usr);
|
||||
s.ClickPagePrimary();
|
||||
var url = s.FindAlertMessage().FindElement(By.TagName("a")).Text;
|
||||
var url = s.Driver.FindElement(By.Id("InvitationUrl")).GetAttribute("data-text");
|
||||
|
||||
s.Logout();
|
||||
s.Driver.Navigate().GoToUrl(url);
|
||||
@@ -404,6 +403,84 @@ namespace BTCPayServer.Tests
|
||||
Assert.Contains("/login", s.Driver.Url);
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
public async Task CanManageUsers()
|
||||
{
|
||||
using var s = CreateSeleniumTester();
|
||||
await s.StartAsync();
|
||||
//Create Users
|
||||
s.RegisterNewUser();
|
||||
var user = s.AsTestAccount();
|
||||
s.Logout();
|
||||
s.GoToRegister();
|
||||
s.RegisterNewUser(true);
|
||||
var admin = s.AsTestAccount();
|
||||
s.GoToHome();
|
||||
s.GoToServer(ServerNavPages.Users);
|
||||
|
||||
// Manage user password reset
|
||||
var rows = s.Driver.FindElements(By.CssSelector("#UsersList tr.user-overview-row"));
|
||||
s.Driver.FindElement(By.Id("SearchTerm")).Clear();
|
||||
s.Driver.FindElement(By.Id("SearchTerm")).SendKeys(user.RegisterDetails.Email);
|
||||
s.Driver.FindElement(By.Id("SearchTerm")).SendKeys(Keys.Enter);
|
||||
rows = s.Driver.FindElements(By.CssSelector("#UsersList tr.user-overview-row"));
|
||||
Assert.Single(rows);
|
||||
Assert.Contains(user.RegisterDetails.Email, rows.First().Text);
|
||||
s.Driver.FindElement(By.CssSelector("#UsersList tr.user-overview-row:first-child .reset-password")).Click();
|
||||
s.Driver.WaitForElement(By.Id("Password")).SendKeys("Password@1!");
|
||||
s.Driver.FindElement(By.Id("ConfirmPassword")).SendKeys("Password@1!");
|
||||
s.ClickPagePrimary();
|
||||
Assert.Contains("Password successfully set", s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success).Text);
|
||||
|
||||
// Manage user status (disable and enable)
|
||||
// Disable user
|
||||
s.Driver.FindElement(By.Id("SearchTerm")).Clear();
|
||||
s.Driver.FindElement(By.Id("SearchTerm")).SendKeys(user.RegisterDetails.Email);
|
||||
s.Driver.FindElement(By.Id("SearchTerm")).SendKeys(Keys.Enter);
|
||||
rows = s.Driver.FindElements(By.CssSelector("#UsersList tr.user-overview-row"));
|
||||
Assert.Single(rows);
|
||||
Assert.Contains(user.RegisterDetails.Email, rows.First().Text);
|
||||
s.Driver.FindElement(By.CssSelector("#UsersList tr.user-overview-row:first-child .disable-user")).Click();
|
||||
s.Driver.WaitForElement(By.Id("ConfirmContinue")).Click();
|
||||
Assert.Contains("User disabled", s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success).Text);
|
||||
//Enable user
|
||||
s.Driver.FindElement(By.Id("SearchTerm")).Clear();
|
||||
s.Driver.FindElement(By.Id("SearchTerm")).SendKeys(user.RegisterDetails.Email);
|
||||
s.Driver.FindElement(By.Id("SearchTerm")).SendKeys(Keys.Enter);
|
||||
rows = s.Driver.FindElements(By.CssSelector("#UsersList tr.user-overview-row"));
|
||||
Assert.Single(rows);
|
||||
Assert.Contains(user.RegisterDetails.Email, rows.First().Text);
|
||||
s.Driver.FindElement(By.CssSelector("#UsersList tr.user-overview-row:first-child .enable-user")).Click();
|
||||
s.Driver.WaitForElement(By.Id("ConfirmContinue")).Click();
|
||||
Assert.Contains("User enabled", s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success).Text);
|
||||
|
||||
// Manage user details (edit)
|
||||
s.Driver.FindElement(By.Id("SearchTerm")).Clear();
|
||||
s.Driver.FindElement(By.Id("SearchTerm")).SendKeys(user.RegisterDetails.Email);
|
||||
s.Driver.FindElement(By.Id("SearchTerm")).SendKeys(Keys.Enter);
|
||||
rows = s.Driver.FindElements(By.CssSelector("#UsersList tr.user-overview-row"));
|
||||
Assert.Single(rows);
|
||||
Assert.Contains(user.RegisterDetails.Email, rows.First().Text);
|
||||
s.Driver.FindElement(By.CssSelector("#UsersList tr.user-overview-row:first-child .user-edit")).Click();
|
||||
s.Driver.WaitForElement(By.Id("Name")).SendKeys("Test User");
|
||||
s.ClickPagePrimary();
|
||||
Assert.Contains("User successfully updated", s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success).Text);
|
||||
|
||||
// Manage user deletion
|
||||
s.GoToServer(ServerNavPages.Users);
|
||||
s.Driver.FindElement(By.Id("SearchTerm")).Clear();
|
||||
s.Driver.FindElement(By.Id("SearchTerm")).SendKeys(user.RegisterDetails.Email);
|
||||
s.Driver.FindElement(By.Id("SearchTerm")).SendKeys(Keys.Enter);
|
||||
rows = s.Driver.FindElements(By.CssSelector("#UsersList tr.user-overview-row"));
|
||||
Assert.Single(rows);
|
||||
Assert.Contains(user.RegisterDetails.Email, rows.First().Text);
|
||||
s.Driver.FindElement(By.CssSelector("#UsersList tr.user-overview-row:first-child .delete-user")).Click();
|
||||
s.Driver.WaitForElement(By.Id("ConfirmContinue")).Click();
|
||||
Assert.Contains("User deleted", s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success).Text);
|
||||
|
||||
s.Driver.AssertNoError();
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
public async Task CanRequireApprovalForNewAccounts()
|
||||
{
|
||||
@@ -747,6 +824,14 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.FindElement(By.CssSelector("[data-invoice-state-badge] .dropdown-toggle")).Click();
|
||||
s.Driver.FindElements(By.CssSelector("[data-invoice-state-badge] .dropdown-menu button"))[0].Click();
|
||||
TestUtils.Eventually(() => Assert.Contains("Settled (marked)", s.Driver.PageSource));
|
||||
|
||||
// zero amount invoice should redirect to receipt
|
||||
var zeroAmountId = s.CreateInvoice(0);
|
||||
s.GoToUrl($"/i/{zeroAmountId}");
|
||||
Assert.EndsWith("/receipt", s.Driver.Url);
|
||||
Assert.Contains("$0.00", s.Driver.PageSource);
|
||||
s.GoToInvoice(zeroAmountId);
|
||||
Assert.Equal("Settled", s.Driver.FindElement(By.CssSelector("[data-invoice-state-badge]")).Text);
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
@@ -1546,7 +1631,6 @@ namespace BTCPayServer.Tests
|
||||
s.GenerateWallet("BTC", "", false, true);
|
||||
var walletId = new WalletId(storeId, "BTC");
|
||||
s.GoToWallet(walletId, WalletsNavPages.Receive);
|
||||
s.Driver.FindElement(By.Id("generateButton")).Click();
|
||||
var addressStr = s.Driver.FindElement(By.Id("Address")).GetAttribute("data-text");
|
||||
var address = BitcoinAddress.Create(addressStr,
|
||||
((BTCPayNetwork)s.Server.NetworkProvider.GetNetwork("BTC")).NBitcoinNetwork);
|
||||
@@ -1751,7 +1835,6 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.FindElement(By.Id("WalletNav-Receive")).Click();
|
||||
|
||||
//generate a receiving address
|
||||
s.Driver.FindElement(By.CssSelector("button[value=generate-new-address]")).Click();
|
||||
Assert.True(s.Driver.FindElement(By.CssSelector("#address-tab .qr-container")).Displayed);
|
||||
// no previous page in the wizard, hence no back button
|
||||
Assert.True(s.Driver.ElementDoesNotExist(By.Id("GoBack")));
|
||||
@@ -1773,10 +1856,6 @@ namespace BTCPayServer.Tests
|
||||
Assert.NotNull(s.Driver.FindElement(By.CssSelector("[data-value='test-label']")));
|
||||
});
|
||||
|
||||
//unreserve
|
||||
s.Driver.FindElement(By.CssSelector("button[value=unreserve-current-address]")).Click();
|
||||
//generate it again, should be the same one as before as nothing got used in the meantime
|
||||
s.Driver.FindElement(By.CssSelector("button[value=generate-new-address]")).Click();
|
||||
Assert.True(s.Driver.FindElement(By.CssSelector("#address-tab .qr-container")).Displayed);
|
||||
Assert.Equal(receiveAddr, s.Driver.FindElement(By.Id("Address")).GetAttribute("data-text"));
|
||||
TestUtils.Eventually(() =>
|
||||
@@ -1866,7 +1945,7 @@ namespace BTCPayServer.Tests
|
||||
// Check the tx sent earlier arrived
|
||||
s.Driver.FindElement(By.Id($"StoreNav-Wallet{cryptoCode}")).Click();
|
||||
s.Driver.WaitWalletTransactionsLoaded();
|
||||
s.Driver.FindElement(By.PartialLinkText(tx.ToString()));
|
||||
s.Driver.FindElement(By.CssSelector($"[data-text='{tx}']"));
|
||||
|
||||
var walletTransactionUri = new Uri(s.Driver.Url);
|
||||
|
||||
@@ -1894,13 +1973,13 @@ namespace BTCPayServer.Tests
|
||||
var jack = new Key().PubKey.Hash.GetAddress(Network.RegTest);
|
||||
SetTransactionOutput(s, 0, jack, 0.01m);
|
||||
s.Driver.FindElement(By.Id("SignTransaction")).Click();
|
||||
|
||||
s.Driver.WaitForElement(By.CssSelector("button[value=broadcast]"));
|
||||
Assert.Contains(jack.ToString(), s.Driver.PageSource);
|
||||
Assert.Contains("0.01000000", s.Driver.PageSource);
|
||||
Assert.EndsWith("psbt/ready", s.Driver.Url);
|
||||
s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).Click();
|
||||
Assert.Equal(walletTransactionUri.ToString(), s.Driver.Url);
|
||||
var bip21 = invoice.EntityToDTO(s.Server.PayTester.GetService<Dictionary<PaymentMethodId, IPaymentMethodBitpayAPIExtension>>()).CryptoInfo.First().PaymentUrls.BIP21;
|
||||
var bip21 = invoice.EntityToDTO(s.Server.PayTester.GetService<Dictionary<PaymentMethodId, IPaymentMethodBitpayAPIExtension>>(), s.Server.PayTester.GetService<CurrencyNameTable>()).CryptoInfo.First().PaymentUrls.BIP21;
|
||||
//let's make bip21 more interesting
|
||||
bip21 += "&label=Solid Snake&message=Snake? Snake? SNAAAAKE!";
|
||||
var parsedBip21 = new BitcoinUrlBuilder(bip21, Network.RegTest);
|
||||
@@ -2684,9 +2763,9 @@ namespace BTCPayServer.Tests
|
||||
var sums = cartData.FindElements(By.CssSelector("tfoot tr"));
|
||||
Assert.Equal(2, items.Count);
|
||||
Assert.Equal(4, sums.Count);
|
||||
Assert.Contains("Manual entry 1", items[0].FindElement(By.CssSelector("th")).Text);
|
||||
Assert.Contains("Custom Amount 1", items[0].FindElement(By.CssSelector("th")).Text);
|
||||
Assert.Contains("1 234,00 €", items[0].FindElement(By.CssSelector("td")).Text);
|
||||
Assert.Contains("Manual entry 2", items[1].FindElement(By.CssSelector("th")).Text);
|
||||
Assert.Contains("Custom Amount 2", items[1].FindElement(By.CssSelector("th")).Text);
|
||||
Assert.Contains("0,56 €", items[1].FindElement(By.CssSelector("td")).Text);
|
||||
Assert.Contains("Subtotal", sums[0].FindElement(By.CssSelector("th")).Text);
|
||||
Assert.Contains("1 234,56 €", sums[0].FindElement(By.CssSelector("td")).Text);
|
||||
@@ -2707,9 +2786,9 @@ namespace BTCPayServer.Tests
|
||||
sums = paymentDetails.FindElements(By.CssSelector("tr.sums-data"));
|
||||
Assert.Equal(2, items.Count);
|
||||
Assert.Equal(4, sums.Count);
|
||||
Assert.Contains("Manual entry 1", items[0].FindElement(By.CssSelector(".key")).Text);
|
||||
Assert.Contains("Custom Amount 1", items[0].FindElement(By.CssSelector(".key")).Text);
|
||||
Assert.Contains("1 234,00 €", items[0].FindElement(By.CssSelector(".val")).Text);
|
||||
Assert.Contains("Manual entry 2", items[1].FindElement(By.CssSelector(".key")).Text);
|
||||
Assert.Contains("Custom Amount 2", items[1].FindElement(By.CssSelector(".key")).Text);
|
||||
Assert.Contains("0,56 €", items[1].FindElement(By.CssSelector(".val")).Text);
|
||||
Assert.Contains("Subtotal", sums[0].FindElement(By.CssSelector(".key")).Text);
|
||||
Assert.Contains("1 234,56 €", sums[0].FindElement(By.CssSelector(".val")).Text);
|
||||
@@ -2770,7 +2849,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.Contains("1 x 1,00 € = 1,00 €", items[0].FindElement(By.CssSelector("td")).Text);
|
||||
Assert.Contains("Green Tea", items[1].FindElement(By.CssSelector("th")).Text);
|
||||
Assert.Contains("2 x 1,00 € = 2,00 €", items[1].FindElement(By.CssSelector("td")).Text);
|
||||
Assert.Contains("Manual entry 1", items[2].FindElement(By.CssSelector("th")).Text);
|
||||
Assert.Contains("Custom Amount 1", items[2].FindElement(By.CssSelector("th")).Text);
|
||||
Assert.Contains("1,23 €", items[2].FindElement(By.CssSelector("td")).Text);
|
||||
Assert.Contains("Total", sums[0].FindElement(By.CssSelector("th")).Text);
|
||||
Assert.Contains("4,23 €", sums[0].FindElement(By.CssSelector("td")).Text);
|
||||
@@ -2789,7 +2868,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.Contains("1 x 1,00 € = 1,00 €", items[0].FindElement(By.CssSelector(".val")).Text);
|
||||
Assert.Contains("Green Tea", items[1].FindElement(By.CssSelector(".key")).Text);
|
||||
Assert.Contains("2 x 1,00 € = 2,00 €", items[1].FindElement(By.CssSelector(".val")).Text);
|
||||
Assert.Contains("Manual entry 1", items[2].FindElement(By.CssSelector(".key")).Text);
|
||||
Assert.Contains("Custom Amount 1", items[2].FindElement(By.CssSelector(".key")).Text);
|
||||
Assert.Contains("1,23 €", items[2].FindElement(By.CssSelector(".val")).Text);
|
||||
Assert.Contains("Total", sums[0].FindElement(By.CssSelector(".key")).Text);
|
||||
Assert.Contains("4,23 €", sums[0].FindElement(By.CssSelector(".val")).Text);
|
||||
@@ -3014,7 +3093,7 @@ namespace BTCPayServer.Tests
|
||||
// Topup Invoice test
|
||||
var i = s.CreateInvoice(storeId, null, cryptoCode);
|
||||
s.GoToInvoiceCheckout(i);
|
||||
var lnurl = s.Driver.FindElement(By.CssSelector("#Lightning_BTC-LNURL .truncate-center-start")).Text;
|
||||
var lnurl = s.Driver.FindElement(By.CssSelector("#Lightning_BTC-LNURL .truncate-center")).GetAttribute("data-text");
|
||||
var parsed = LNURL.LNURL.Parse(lnurl, out var tag);
|
||||
var fetchedReuqest =
|
||||
Assert.IsType<LNURL.LNURLPayRequest>(await LNURL.LNURL.FetchInformation(parsed, new HttpClient()));
|
||||
@@ -3051,7 +3130,7 @@ namespace BTCPayServer.Tests
|
||||
i = s.CreateInvoice(storeId, 0.0000001m, cryptoCode);
|
||||
s.GoToInvoiceCheckout(i);
|
||||
// BOLT11 is also displayed for standard invoice (not LNURL, even if it is available)
|
||||
var bolt11 = s.Driver.FindElement(By.CssSelector("#Lightning_BTC-LN .truncate-center-start")).Text;
|
||||
var bolt11 = s.Driver.FindElement(By.CssSelector("#Lightning_BTC-LN .truncate-center")).GetAttribute("data-text");
|
||||
BOLT11PaymentRequest.Parse(bolt11, s.Server.ExplorerNode.Network);
|
||||
var invoiceId = s.Driver.Url.Split('/').Last();
|
||||
using (var resp = await s.Server.PayTester.HttpClient.GetAsync("BTC/lnurl/pay/i/" + invoiceId))
|
||||
@@ -3104,7 +3183,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
i = s.CreateInvoice(storeId, null, cryptoCode);
|
||||
s.GoToInvoiceCheckout(i);
|
||||
lnurl = s.Driver.FindElement(By.CssSelector("#Lightning_BTC-LNURL .truncate-center-start")).Text;
|
||||
lnurl = s.Driver.FindElement(By.CssSelector("#Lightning_BTC-LNURL .truncate-center")).GetAttribute("data-text");
|
||||
Assert.StartsWith("lnurlp", lnurl);
|
||||
LNURL.LNURL.Parse(lnurl, out tag);
|
||||
|
||||
@@ -3117,7 +3196,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.Contains($"{cryptoCode} Lightning settings successfully updated", s.FindAlertMessage().Text);
|
||||
var invForPP = s.CreateInvoice(null, cryptoCode);
|
||||
s.GoToInvoiceCheckout(invForPP);
|
||||
lnurl = s.Driver.FindElement(By.CssSelector("#Lightning_BTC-LNURL .truncate-center-start")).Text;
|
||||
lnurl = s.Driver.FindElement(By.CssSelector("#Lightning_BTC-LNURL .truncate-center")).GetAttribute("data-text");
|
||||
LNURL.LNURL.Parse(lnurl, out tag);
|
||||
|
||||
// Check that pull payment has lightning option
|
||||
|
@@ -25,6 +25,7 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
public class ServerTester : IDisposable
|
||||
{
|
||||
public const string DefaultConnectionString = "User ID=postgres;Include Error Detail=true;Host=127.0.0.1;Port=39372;Database=btcpayserver";
|
||||
public List<IDisposable> Resources = new List<IDisposable>();
|
||||
readonly string _Directory;
|
||||
|
||||
@@ -50,19 +51,16 @@ namespace BTCPayServer.Tests
|
||||
PayTester = new BTCPayServerTester(TestLogs, LoggerProvider, Path.Combine(_Directory, "pay"))
|
||||
{
|
||||
NBXplorerUri = ExplorerClient.Address,
|
||||
TestDatabase = Enum.Parse<TestDatabases>(GetEnvironment("TESTS_DB", TestDatabases.Postgres.ToString()), true),
|
||||
// TODO: The fact that we use same conn string as development database can cause huge problems with tests
|
||||
// since in dev we already can have some users / stores registered, while on CI database is being initalized
|
||||
// for the first time and first registered user gets admin status by default
|
||||
Postgres = GetEnvironment("TESTS_POSTGRES", "User ID=postgres;Include Error Detail=true;Host=127.0.0.1;Port=39372;Database=btcpayserver"),
|
||||
Postgres = GetEnvironment("TESTS_POSTGRES", DefaultConnectionString),
|
||||
ExplorerPostgres = GetEnvironment("TESTS_EXPLORER_POSTGRES", "User ID=postgres;Include Error Detail=true;Host=127.0.0.1;Port=39372;Database=nbxplorer"),
|
||||
MySQL = GetEnvironment("TESTS_MYSQL", "User ID=root;Host=127.0.0.1;Port=33036;Database=btcpayserver")
|
||||
};
|
||||
if (newDb)
|
||||
{
|
||||
var r = RandomUtils.GetUInt32();
|
||||
PayTester.Postgres = PayTester.Postgres.Replace("btcpayserver", $"btcpayserver{r}");
|
||||
PayTester.MySQL = PayTester.MySQL.Replace("btcpayserver", $"btcpayserver{r}");
|
||||
TestLogs.LogInformation($"Database used: btcpayserver{r}");
|
||||
}
|
||||
PayTester.Port = int.Parse(GetEnvironment("TESTS_PORT", Utils.FreeTcpPort().ToString(CultureInfo.InvariantCulture)), CultureInfo.InvariantCulture);
|
||||
@@ -85,7 +83,7 @@ namespace BTCPayServer.Tests
|
||||
File.Copy(file, Path.Combine(langdir, Path.GetFileName(file)));
|
||||
}
|
||||
|
||||
#if ALTCOINS
|
||||
|
||||
public void ActivateLTC()
|
||||
{
|
||||
LTCExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_LTCRPCCONNECTION", "server=http://127.0.0.1:43783;ceiwHEbqWI83:DwubwWsoo3")), NetworkProvider.GetNetwork<BTCPayNetwork>("LTC").NBitcoinNetwork);
|
||||
@@ -100,12 +98,7 @@ namespace BTCPayServer.Tests
|
||||
PayTester.Chains.Add("LBTC");
|
||||
PayTester.LBTCNBXplorerUri = LBTCExplorerClient.Address;
|
||||
}
|
||||
public void ActivateETH()
|
||||
{
|
||||
PayTester.Chains.Add("ETH");
|
||||
}
|
||||
|
||||
#endif
|
||||
public void ActivateLightning()
|
||||
{
|
||||
ActivateLightning(LightningConnectionType.CLightning);
|
||||
@@ -239,7 +232,7 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
#if ALTCOINS
|
||||
|
||||
public RPCClient LTCExplorerNode
|
||||
{
|
||||
get; set;
|
||||
@@ -248,15 +241,12 @@ namespace BTCPayServer.Tests
|
||||
public RPCClient LBTCExplorerNode { get; set; }
|
||||
public ExplorerClient LTCExplorerClient { get; set; }
|
||||
public ExplorerClient LBTCExplorerClient { get; set; }
|
||||
#endif
|
||||
|
||||
public ExplorerClient ExplorerClient
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
readonly HttpClient _Http = new HttpClient();
|
||||
|
||||
public BTCPayServerTester PayTester
|
||||
{
|
||||
get; set;
|
||||
|
@@ -527,7 +527,7 @@ retry:
|
||||
{
|
||||
var server = new FakeServer();
|
||||
await server.Start();
|
||||
var client = await CreateClient(Policies.CanModifyStoreWebhooks);
|
||||
var client = await CreateClient(Policies.CanModifyWebhooks);
|
||||
var wh = await client.CreateWebhook(StoreId, new CreateStoreWebhookRequest()
|
||||
{
|
||||
AutomaticRedelivery = false,
|
||||
@@ -669,7 +669,7 @@ retry:
|
||||
var db = (NpgsqlConnection)dbContext.Database.GetDbConnection();
|
||||
await db.OpenAsync();
|
||||
bool isHeader = true;
|
||||
using (var writer = db.BeginTextImport("COPY \"Invoices\" (\"Id\",\"Blob\",\"Created\",\"CustomerEmail\",\"ExceptionStatus\",\"ItemCode\",\"OrderId\",\"Status\",\"StoreDataId\",\"Archived\",\"Blob2\") FROM STDIN DELIMITER ',' CSV HEADER"))
|
||||
using (var writer = db.BeginTextImport("COPY \"Invoices\" (\"Id\",\"Blob\",\"Created\",\"ExceptionStatus\",\"Status\",\"StoreDataId\",\"Archived\",\"Blob2\") FROM STDIN DELIMITER ',' CSV HEADER"))
|
||||
{
|
||||
foreach (var invoice in oldInvoices)
|
||||
{
|
||||
@@ -698,11 +698,13 @@ retry:
|
||||
await writer.FlushAsync();
|
||||
}
|
||||
isHeader = true;
|
||||
using (var writer = db.BeginTextImport("COPY \"Payments\" (\"Id\",\"Blob\",\"InvoiceDataId\",\"Accounted\",\"Blob2\",\"Type\") FROM STDIN DELIMITER ',' CSV HEADER"))
|
||||
using (var writer = db.BeginTextImport("COPY \"Payments\" (\"Id\",\"Blob\",\"InvoiceDataId\",\"Accounted\",\"Blob2\",\"PaymentMethodId\") FROM STDIN DELIMITER ',' CSV HEADER"))
|
||||
{
|
||||
foreach (var invoice in oldPayments)
|
||||
{
|
||||
var localPayment = invoice.Replace("3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd", storeId);
|
||||
// Old data could have Type to null.
|
||||
localPayment += "UNKNOWN";
|
||||
await writer.WriteLineAsync(localPayment);
|
||||
}
|
||||
await writer.FlushAsync();
|
||||
|
@@ -604,7 +604,7 @@
|
||||
},
|
||||
"expectedProperties": {
|
||||
"Created": "04/23/2019 18:27:56 +00:00",
|
||||
"Type": "BTC-CHAIN",
|
||||
"PaymentMethodId": "BTC-CHAIN",
|
||||
"Currency": "BTC",
|
||||
"Status": "Settled",
|
||||
"Amount": "0.07299962",
|
||||
@@ -634,7 +634,7 @@
|
||||
},
|
||||
"expectedProperties": {
|
||||
"Created": "10/01/2018 14:13:22 +00:00",
|
||||
"Type": "BTC-CHAIN",
|
||||
"PaymentMethodId": "BTC-CHAIN",
|
||||
"Currency": "BTC",
|
||||
"Status": "Settled",
|
||||
"Amount": "0.00017863",
|
||||
@@ -666,7 +666,7 @@
|
||||
"Created": "03/21/2024 07:24:35 +00:00",
|
||||
"CreatedInMs": "1711005875969",
|
||||
"Amount": "0.00000001",
|
||||
"Type": "BTC-LNURL",
|
||||
"PaymentMethodId": "BTC-LNURL",
|
||||
"Currency": "BTC"
|
||||
}
|
||||
},
|
||||
@@ -697,7 +697,7 @@
|
||||
"Created": "03/20/2024 22:39:08 +00:00",
|
||||
"CreatedInMs": "1710974348741",
|
||||
"Amount": "0.00197864",
|
||||
"Type": "BTC-CHAIN",
|
||||
"PaymentMethodId": "BTC-CHAIN",
|
||||
"Currency": "BTC",
|
||||
"Status": "Settled",
|
||||
"Accounted": null
|
||||
|
@@ -1,11 +1,11 @@
|
||||
Id,Blob,Created,CustomerEmail,ExceptionStatus,ItemCode,OrderId,Status,StoreDataId,Archived,Blob2
|
||||
Q7RqoHLngK9svM4MgRyi9y,\x1f8b0800000000000003c454cb76a24010dde72b7258cf180d08929d0a89899ae323ea4962163c0ae808ddd8344426c72f9bc57cd2fcc2340d31ea38b398cd2cfb5657d5add7fdf9fdc7fbd9f9b9845ce9ea5c1a6b9335e90db0dfd7936ca80cfd498ef45cfa52fc4818a1702bbec9893feb76d9ace3abd3d6e06e456dd76b2fece46eb8eee4d7032f9bae5f6fd44dbfb330ddd2995017a870c6691896f16200774442e4e41cae0b8c5a0cf8436dea6a4d56344d5135596e2ac286704690030f282abe349a724bd6e5a67c298cb089117746041fd815a5b2bb109304b1b6eb524812514347ef1bb2a6cd0686663e7a8b4779dcf3bd45eb5ae9bcb181e10f50c93ca6c44d1d768b3d422391817b172d2b2831880c489c229e0d4085478577890b7be51691823c418e1572d4b3c2043e60caabe29856ab578893520a58b4459a4d0d89a35bc1c54e73dec5534c84e5de8a8e520ad88c2c149ec0bb24c58ce6272c4f283e814e59399ddfe220762a48d5ebcb3f9b1a274ca380e08f24bbbaf9ec0c8b59fbdbc3d70965a20953566c8d2fbab589535b35dab61f78e9a4bb50c76b2af7bda9f18a56caca9ca1acbdf202e7a6375ccd4d50eca775539e8fbf69fa4a514c326e0e8d7870d7efb747fd66369faba38d366b4c641dc6c8c67660240b3d35c78dbe1be85dc38efcc9d7e7f832095ea4d38c1088457b5f4a9d87ee52ba5afe277a4b69fb71c1164b05270c6f5275370ec425e7cab6eb706ce511605660cf2fe575829762d7b243d85fe10a1e1e2e19475d44c161b3c9a0c818301627571717919530a0981f0767b342d8af39242ab9b0cd3588d3ad9762e0f150f784218f1f4d41b160c2685a26c57b8632c5a7b000cd80ce68789817610cace642446a36737875e5bf1aa17e99dfa179cc48b568d55df1c9ed1e7fd7a7f296cb9e0d8105a410bbf7edcee4014c4aefc60e3baa5860ffa454c279bbbb978860c4d59a77d7dce9e24e135bf54a130354487aa14855323898bf95f18916c3aeac3d2b090e7fc0860176c13d1ed2e76a40566dd0f1563d9010a88585f0356af5b3ed2f000000ffff030035140a5d88060000,2018-10-01 11:32:12+00,,,,,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
|
||||
Ka6GHBrFJPRwFRga1RD6Yz,\x1f8b0800000000000003c454cd769a4014dee7297258b74682a066a7427e24e6284a729a9fc5001798083364188826c727eba28fd457e830101353db45375dcef7cdbdf7bbbf3fbfff783d383c5470a09c1c2a3632cece87ec743c759e4f9d08a98e697c7b51be543f724e195cc86f5a1eb9a31177879131ef5d8e97cc0bc2c18d978f274fc3f5e96558ce9f1ecf8c953dbcb182da98b2009834264592d4fe3280604a13ecaf05dc9618431cc44337ba46abad6aed76db50b5635d72989414fbb0c069f545d5b59ed6eff4f4da10561916c698921d5eef367c0019cd311f0401833c973998b7a5e742ef7110a7fd65e9f62889c7b6f5726b777d3fc25c9fd5ca334683c2e71724a42c9511847555b24a1287d484dcaffc9d310072b80024cd1a724403f89073e52e5ee7d84789404394e4f00633915a558656fbb881fc823120b2388ae53a8a4037529157ac452df7e991cc154a3fc594b095229cecc147b4209cadf730b738db83ce79dda3dffc60becf4953f1e33f53ea1e6a1a53f216649bb7e8a08938fa384362a870298b30e7d5ec44b25aabacf00c73e045715838a31b63f6c4343b9c9b8f78d9595a2e2e07cb30f6cfce27cb6b0b3adeed93ae5dcf5ebafd65a763d1993e31b3cbb16d0fa6b65e5e5f1bd355d7551dad0f33ec112f36f39b7e61cd543b88fb23d34b23e7eb5d769cc70fca7e4518e4b8bdde2bc3c5e85e39b9ff4ff2ee95cddb1e235e484d049e95667b7cc86acd0db7ad7086d629105e61770ff58e4258900079097c9ce1069eec0e994003ccc0e7ae7359458c39cff293a3a314e51c1811db21d42c31895a3e4d6b2d7c750a7281dbf5e686c2d515e538145b5349ac947056d441c907a20ef17e5e8095c05c96ecc6c584006f0590d296c77d915dfdaf455954c7f7d93ae3b419b466af44e7b68fbf5fa97a99eb9a4d80c7b43a79af9b2d150238b5b5bac53e652cb17fba57d278b3dd9794122c6eb6a8aeb5bd8edbcbd8d79acb18e3eab05727a909063bfd47a5e868d5ec863d4779bcfb03561c4800c1e726bd8f0694cd047d9eaa054d8021222f9fda6a1f6c7e010000ffff0300ddecc9e08e060000,2018-10-01 11:54:10+00,,,,,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
|
||||
Q3kZ3F8cUD57WUqcc8QLs2,\x1f8b0800000000000003c4544972da4014ddfb1494d6099110a377806463a662b0c085f142c3176a23758b564b86b83859163952ae90564b96812259649365bff77ffff9fdfaf1f3fda654929023dd96a4a9ba5da9774ddbd06a8da5b1b3ede6741855a42fa945c408850761a6461ba3db654667539f3787fd2db51cb7bdb4a2fe68d739dc0ddd64be7bbdafef079da5ee64ce843a4085338e7d3ffb2f047026c447f681c3b2c0a8c9803f6af546bd2c2baa2ccb7545add404877042900d8f28484d949ada545bd566a32248d887883b2382cff85a23e71d08498458db71284491a861921876af3f1ec361bc5bb4d427dadb57bcfba145f4fd66fe36795a6599879438b1cd1eb04b68202270efb465694a0c020d223bfd6f64460c28260e94e6ccdc22bc11feb95597e327c5a7ff7a8708d9a6cf51d7f423f88029af916395b29c23764c2960d12449376612478f22332b3ef09e5ecb4b306333b80829603d30917f05ef9218337ab8c2ac507805e545b26bff7711bbf649def9ca9f29e50a35f108fe0852d4cd27a999cc3cdd25be5c28114d98b3748736a25bfb30b6ea5adbda786e3ceb2eebd31d5507ee5c7b45dbea563750d2deba9e7ddf1b6d173a54add5aea62ea6df1bad6db5aa93696da485c3fe60d09e0c6ac962519fec1b8632535b304516b63c2d5ab6627daa0c1cafd5d5ac6033fbfa1c5622ef45ba9e1102b176ef6ba9f3d85d4bb7ebff94de5a3a7edcb3c9629113863729bf221bc22ce79c2b3a1c9a8700304bb1e797ec56c18db1635a3e9cae700e8fce978ca30ea2603363364c237a8c85d1edb76f417134517633659b04592e6c7f07e290e54c1a5cfed59830e4f2a349534c336134ce82e213220bf129334013a006f5cfe3228c81951d0848d96236af2eb32b139addad64d343c848be68f95df1c9158fbfab5576cb59cf46c03c924adffbb1a05c8059e6ad14d845c502fb27dd12cec7e25e028211d76ede5dbd50c942215b6aae901e4a053e55a43c189ccddf4cf844d361e76ccf8cbc730bd833c00e389743fa5c0d48f20dbadcaa47e20335b1103ea52cdf1c7f030000ffff03003e6b8efb96060000,2018-10-01 11:54:32+00,,,,,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
|
||||
FSktP1Nrxu7arh7TUFAgTZ,\x1f8b0800000000000003c4554b72e33610ddcf295c5c27b23e16297a1599923c63591efd55f1781620d1143124011a0035545c3a59163952ae10008415d9a54caab2c992af5f773ff48f7ffefec7cb878b0b8760e7fac2192d52396d3df0aaf4104fbce56ad4df2e1f9d9f344348c6e193a175c47615047275b37517bdfbbb948738ee6f42713779bed98feee3dde2f9dbad5b8d6f36435c3b338e81d7ce41a922e59f2d50872e00f0946524da2b46d3601c49501f5dd76b377a6dbfedf7bc96e7f9c646e88e91089624d79456b7d3ebf8dd66a76b8c501544391346dfda7d6bc7503041641f630e4298e70c969bd093de7886d63de1a2ea7b67ddc28fb76595dcfdba1db9eeb296597086cb487ea231e3b9c9a0bc75f5b42409f90044a4e34d9090c029c370b1902825746bfc2d2b50b862d132cb2c5a247b41229429344699805798ab376afdcd66a369b1a8e41ca82993335ccd1d851e8cb6b0dcab7a9e53662c0f287f97d4c0c31c119d56c5d54d01fe0b54282f3268442c774e99012ba9e4fb33311e497106550f97e73206449e0b62bbd1fe6753eb8c699a30fa9ae45809d5dd0192e884ae5acec9ce946521f55c6d4dfdaaa20cdd413fdc2671390f36eeec9977c6f162f08da457e9704576fd344ea2db8f93743d84abf0f1b9db59cf7ef3fcf4ea6ac866ddc9a0b8bf1b8ffbd37177b75ebbd3ca5bb5e61d1f6624a46132101bbf1cce5a639cf8c120ccb7f39fbf146d917c75ce2b226046f1e5c9b959064fcef5d3ff24efc939bcae3b92a5d144e1bb63372b82a2d66c6dc70a17689f03951afbf2b5de5f884b8a5198c1e9585b78f26f63a778987088e46a7eaf89899485b8bebc3ce15dca04d154ec59597bc86a04765dcc77acb43d304962b55a5ab4d6267959cba027861fa4504b9985284a85ad09f01df015cf4ef96a852805d9c090b3462823558a9ad760bc5e7c27e2fb42323b95762d559b8f1f3f3e77f531a80b3c0199307d465f0e47530c30afbd5b47ec5d310cf69f0e9f713e1c972b6794a8ff803a69c3e3993d9e58bf6b4f6c42f4cf429f349b0cde0c0bdaa9f6ebc9b0d68f48246f195049a018f0fbfefd3d47b0b3e3f67e04972c038e687d391bcd0f87bf000000ffff030075db901fe2060000,2018-10-01 11:57:15+00,customer@example.com,,,CustomOrderId,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
|
||||
HuzCsv9hghew2FD6zVqUyY,\x1f8b0800000000000003c455cb76d33010ddf3153d5e439aa75377456aa794268126a913286521dbe358b52d3992ecc6edc997b1e093f80564590d694f8073d8b0f49d3b3357f3f28f6fdf1f5f1d1d1938304e8f8c8bfcc1e68515ad22b86f9f3be6c362ed969f8dd715830bcae0bda275f8cab56de19eadccf9c9f832665e100e961ebf9caccfcaf37158ccd777efcccde86c390c6a67ca0260b5b39dcb48e9470dd4a13380e08a26d82f25a3a9308604c88f9ed96f374eda56db3ae9b7fa7d4bd9302928f6e11aa715a5d5eb9c74ac5edfea28236c322c9d3125cfeca6a9ed01649463310802069cabe758e9f2be701d7ad3b3379f9069a72be406a865ad3fdf59e36ceab46a9919a341ee8bf724a42c5519a47755bd4a9280d401ee57f126880b60840670341728c664a5fc35cb96b864913c49349a4525c73e4a241aa284c313cce41b2bfdcd66a3a9313f670c882a933174678644b74a9b9797b29e879429cb0794be48aae0618a709556c6ad9a02ec2d6c509a25d0f0696aec336d9a13c1ca03316e707600950f178732da581c0aa2bbd1febda975c0741551f294645709d95d0709b447972d67b85065998b6aae56aa7e9b2cf74c67e0ada2309fd94b73ba669d513877ee70dc8d872e2e067118f9ef2e26f162085def66ddeb2ca60f7d2bee768774da9b38d9f872341a5c8d7ac562615e6dfa6e6bd6b1608a3de2450e5f5af970da1a0591653b5eba9abdf992b579f4d538ac08831ac5c75be3ecdabe354e6fff93bc5b63fbb4ee48e44a13817b436f960f59ad59db7615ce5099021115f6e56bbdbf10e624405e02fb63ade1c9dfc64ef202ccc017ee6c5c112321327e7a7cbcc73b161122312f695e7b88cd39e87551dfa1d4f6810a1ccad5aa4457da04cb6b1964cff087147229130ff931d7350156007359b2cf972b4408884600296d78c297a5a8790dcaeac5377c566682eaa9d46b29dbbcfbf8f3b9ab8f415de00988885667f471bb338500b3dabbb5c35e144361ff74f894f376b75c292558fe07e4491beeceac3eb1dd66b3ad4f6c84ab9f4575d2743278362ca890edaf26435b2f108f9e336023800410bcecdfaf3982428fdbcb11bca6093044eacbd968bedafe040000ffff030069b828dae2060000,2018-10-01 12:09:53+00,customer@example.com,,,CustomOrderId,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
|
||||
YZV1PQasUKEXdSkSV69XDL,\x1f8b0800000000000003c455cb76db3610dde72b7cb86e65911429cbabca1463c792523d2c39759c05480c4584244003202bd5475fd6453e29bf50108455d9474dcfe9a64bdeb93373312f7efff3dbf3bbb3338b60ebf2ccfaed616dcfe648acc6e127bccc966b7ff06934b17e6a1842320e1f34cd159b5510c8d5d5c65f5e4c6e331ee164781f89dbe9d3d5eefd24a9974f5faffdedf8ea3ec4ad33e31878eb1c542a52f1ab01dad025009eb19cc43bc5e86a8c2309eac3f37da7e3f56cd7f55dcf751c6d23b46624863b523414db732fdc81ef38b636c2b624ca9930facadeb78d1d43c90491438c3908a19f33b85927f2b62f2807a7866c7ce1f5c49dbc76275e9a08e7a2ba6e65969ce12a961f68c278a13328efa67a8d2409c50844dcc49b2221815386e16c295146e846fb1b56a070c5a2559e1bb44c7782c42857688272012f30576f6cf477bb9daec1e28a73a0ba4c56b85a580add6b6d51b553f53ca54c5b3ea2e24d520d8705224d5a15b7690af05f608b8a32874ecc0aeb9819b08a4abe3b11e381942750f570792a6340e4a920a61bce3f9bec13a659cae84b92432554774748a223ba6a3927b52ecb523673b5d1f5db9655e48f86d1264daa4570efcf9fb83b4e96a3af24eb65e18ad4c32c49e3eb9b69b60ea1173d3c79ee7afe477f90f57a219b7bd35139b91d8f87b3b157afd7fe6cdb5fd90b77007312d1281d89fb4115ceed314e07c1282a368b9f3f978e48bf58a71511d0a3f8fc685ddd058fd6e5e3ff24efd1dabfac3b9295d644e177cb6c560c65abd9d80e152ed1ae002a1becf397767f21a92846510ec7636de0e9bf8d9de261c22196abc5a421a65296e2f2fcfc88772e534433b16355eb21b7efc1ac8bfe4e94b68f4c9244ad5623bad12679d5caa047861fa4504b994728ce84a909f01af88ae7c77cb5429482ec6028582792b12a45cbeb30de2ebe15f35d2999994ab396aacd878f1f9fbbf618b4059e824c5973469ff7075302b068bded03f6a6181afb4f874f3bef0fcb55304ad47f409db4f07066cd89ed75bb7d736253d2fc2c9a936692c1ab6141b56a7f3319c67a8344fa9a015b0914037edbbfbfe7086a336e6f47f08ee5c0116d2f67a7fb6eff17000000ffff03003a3b8e5ae2060000,2018-10-01 12:17:01+00,customer@example.com,,,CustomOrderId,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
|
||||
3qtdXmycQwhWEPs91MExB2,\x1f8b0800000000000003c455cb769b4810dde72b7c5827b21002455e4502f2b0a48c1e96e538cea2a10bd106ba5177238bf8e8cb66319f34bf90a6c18aeca324e7cc6696dcba5575bb5efcfbf73f8fafcece0c828d8b33c3da487c9395e1ec215ef953d13727fe6ed8315e570c2119874f354dac97ae2b97c3b5b3783bbe4c7880a3c12a109793cdb07c3f8eb68bcdfd0767371aae7c5c3b338e81d7ce6ea122657f35401d3a07c0539692b0548cb6c63892a03e6cc7e9b4ecae69598e655b9d8eb611ba6524842b925514d3b6de5a7dc7b11d6d845d4e943361f499bd6737760c3913440e30e620847e8ef9b0bcf70a6bf6ddc3e3e48bdfe38e7f39ef4d2837931b424641af969973868b507ea211e399cea0bcabea559224641e88b08a37414202a70cc3d942a284d0b5f66f58aec2158b1669daa0795c0a12a254a1114a053cc15cbd51eb373bad76038605e740759d0c7f393714bad7e282a254053d254d5b3ea3ec45560dfb1922555e15b7ea0af077b043599e422b649971cc745941252f4fc4b825f90954bd5c9ecae812792a48d38eceaf4de609d33466f429c9a112aabd1e92e888ae7acec956976521abc15aebfaedf22270bc41b08ea362eeae9cd9865ba368e1dd93a49bf84bb21d24511c7ef83849ae7de806b71bdbba9e7deff5936ed767337be2e5e3cbd168301dd9dbeb6b67baeb2dcdb9d587190968107b62d52ffc9939c271dff5826c3d7ff335ef88f89b715a11013d8b8f77c6f0cabd332eeefe277977c6fe69df912cb4260a0f46b35a21e4b5e6c676a8708eca0ca8acb0afdfea0586a8a01805291ccf75034ffe34768a870987502ee7e38a184b998b8bf3f323deb98c114d44c98ada43eede83de97767d4122a5ed33932452bb5589aeb4495ed432e891e13729d456a6010a13d1d404f816f892a7c77cb54294826c61c8582b90a12a45cd6b315e6fbe11f23297ac99ca662d559b0f1fbfbf77f535a80b3c0119b3ea8e3eee0fa608605e7b9b07ec453134f69f2e9f76de1f962b6394a81f81ba69fee1ce3637b6db36cde6c6c6a4fa5b5437ad4906cf86056d55fbabc968ac1f91889f3360278162c02ffbf7738e60db8cdbcb11bc62297044ebd3d96abfdaff000000ffff0300fd61e9cae3060000,2018-10-01 12:24:16+00,customer@example.com,,,CustomOrderId,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
|
||||
BtJGr7kt52QkZe8RdhKz42,\x1f8b0800000000000003c455c172da4810bde72b5c3a6f301208249f168462078cd780c115e21c469a161a4b9a9167465a888b2fcb219f945fc86824b3d8c566abf692a3de7b3dfda6a7bbf5e3dbf7e777676706c1c6c5993194e34bde4fa46dcd923538731c4fbe762de38f4a2124e3f051cb3a62b3f43cb91c6e7a0be77a9cf0004783fb408ca74fc3dd87eba85c3c3d5ef6b693e1bd8feb60c631f03ad82bd449d95f0d501f9d03e05b969270a7146d8d7124417dd83dc76a5996d9711dc771db3dcd115a3212c21dc92a8969779c8edb6ff72d4dc236272a9830fa9a771b1e43ce0491038c3908a1af73152d3ee55bfea9fb784978de21ebf286925537598ef12258737f52dbcc39c345283fd288f14c6750d155f52a4b12b21188b03a6f8a84044e1986b3854409a11b1ddfa83c852b152dd2b441f3782748885285462815f002737547eddf6cb79c7ebf6d9b96dd7061c139505d2ec35fce0d85eeb5c7a0d8a9ba9e72a8991b94bd49ae613f43a44a6fa4880b4168028efbe7a6025b21cb8c63a9c70a2af9eec4216b929f405505e4a9941e91a70e699ec5fa77ca3c41ddc68cbe243994423df308497424576fcf49a9ebb29055836d7401b77911f44683601347c5dcbbefcd9e7867122d468f24e926fe929483248ac3cbab69b2f2a11bac9fecce6af6b5ef26ddaecf66f674945f8f2793c1edc42e57abdeedb6bf34e71d176624a0413c12f76ee1cfcc098e5d6f14649bf9fbcfb925e22fc6694704744f3e3f18c33befc1b878f84df61e8cfdcbdc2359684f14fe369a110b21af3d37dca1c239da654065857dfe520f324405c52848e1b8bf1b78fa9f7da784987008e5727e5d2963297371717e0e5b94e52954ba7319239a881d2bea08b9fd007a70daf52a8994b91b2649a486ac725d9993bca87dd023e21729d478a6010a13d1140578097cc9d363bdf24d29c816868cb50219aa5ad4ba16e3f50a3042becb256bdab2194cf5ce878f5f2fbe7a2dd4159e828c59b5509ff7072a0298d7d1e6017b530c8dfdaf15a883f787e9ca1825ea8fa0969b7f58b8cdb2edb64dbb59b631a97e1bd5726b92c1ab6e41a57aeaaa351af60a89f8b502b6122806fcf6fdfe6924289b7e7bdb83772c058e68bd435bed77fb9f000000ffff030034f07389ec060000,2018-10-01 12:31:12+00,customer@gmail.com,,,CustomOrderId,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
|
||||
VWB3riH9WuCv9NtSedaGE6,\x1f8b0800000000000003c455c1729b4810bde72b5c9cb3b21012029f222162c792bc9664a494631f0668c404984133032bc5a52fcb219f945fc83060457629deaabdec91f75e4fbfe9e96e7e7efff1f4eeec4cc3a17671a62d574383e12b7b5538a57d231610a24bd7d4de570a2e28834f4a66f0b5e738c21baecd8535b94e981f468395cfafa79be1eee3242a179baf97e6763c5cb9611d4c5908ac0e760a7952f67703d447e700e12d4d71b0938ab6c21812203f7aa6d569753aba615b9665b74dc56152521cc01dce2a89de332cc3eeebb6ae48d8e65806634a5ef056bbe143c829c76210860c3857d7e96fd6b6037d4f1457c10adb9fbd9bfb8531b059d17326cbcbd5106a9b39a36111884f24a22c5319647455bdca92806c043ca8ce9b222e80111ac2d942a00493b58a6f548ec4a58a1469daa079bce33840a944239472788699bca3f2afb75b56bfdfeee99d5ec30505634054b934d79b6b12dd2b8f7eb193753de5503137287b955cc16e8670955e4b11e31c93042cfbc3ba025b01cdb463a9430b22d8eec421f7383f81ca0a8853291d2c4e1dd23c4be7cf947e82ba8d29794e7228857ce61112e8482edf9ee152d56521aa065bab026ef3c23747037f1d47c5dc5999b30d33c6d162f41527ddc4f5703948a238b8bc9a264b17bafefda6672c67dffa76d2edba74d69b8ef2c9f5783cb81df7cae5d2bcddf63d7d6ed830c33ef1e3115fd9853bd3c7616c3b233f5bcffffa927778fca89d768441f5e4d38336bc731eb48b87ffc9de83b67f9e7b240ae589c03f5a336201e4b5e7863b543847bb0c88a8b02f8ff52043549010f9291cf777034fffb5efa430c40c02e1cd2795321622e717e7e7b045599e42a53b17312209dfd1a28e10db8fa006a75daf92489abba1024772c82ad79539c18ada073922de4821c733f55190f0a628c04a601e4b8ff5d23721205a2164b4e58b40d6a2d6b528ab578016b05d2e68d396cd60ca773e7cbcbdf8eab55057780a22a6d5427dda1fa808605e47eb07ec553114f69f56a00ade1fa62ba304cb3f825c6eee61e136cbb6dbd6cd66d9c6b8fa6d54cbad49062fba0595f2a9abd668d82bc4e3970ad80a202184afdfef772341d9f4dbeb1ebca3293044ea1dda6abfdbff020000ffff0300e1a26ae9ec060000,2018-10-01 12:33:11+00,customer@gmail.com,,,CustomOrderId,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
|
||||
At4CDM5vfewV2WDHEPKRbt,\x1f8b0800000000000003c4554d73a33810bdcfaf4871de758c0db6c9696d20c9c471c61fb15d35933908688c06908824183329ffb23dec4fdabfb092205e27e59daddacb1e79efb5faa9d5ddfcf9fb1f2f1f2e2e0c1c195717c65858ae37b3ab18be6f7a5befd69f4f9781307e510a2e28838f5ad6e7bbb5eb8af56437588deeef521644f1781bf0bbd9f3a4bebe8fabd5f3b79bc17e3ad9fa51134c5904ac09764b7952fea9059aa30b80684e331cd652d1d5184302e4873d18f53b3db33f1c398edd1d3a9ac3a4a23884479c2b8969f74756d71c0d4c4dc2bec0321853f286ef0d5b3e8282722cc651c480737d9db1fde361b901320bf8c4ab83bb38c2dc22d73dd7de06f5eac6228dcd82d1a80cc547125396eb0c325a554f5912907bc04375de0c71018cd0082e5602a598ec747cab72252e55a4ccb2162d929ae31065128d51c6e11566f28ecabf351a76468ee90c7bc3960a4bc680e86a19fe7a6948f4a02d06652dcb7acea0661e50fe2eb786fd1c6195ddc810e31c931446ce6f3b0576429a1ba752979644b0facc219f717106950510e752ba589c3ba47d95de3f53e6196a9e50f29ae4580af9ca1e12e8442e9f9ee14ad76525547fed7401f745190cbc71b04be272e96e078b67d69fc62bef1b4eadd45fe36a9cc64978733b4b373e58c1e767bbbf59fc183aa965f97461cfbce2fe6e3a1dcfa776b5d90ce6fbe1da5cf61d58e0800489c7b74ee92fcc699438ae17e4bbe5af5f8a1e4fbe1ae71d61d02df9f2644c1edd27e3eae97fb2f7641c5ec71e89527b22f0dd68272c84a2f1dc72c70a17a8ce8108857df9dacc31c425895090c1697bb7f0ec5ffb4e0a23cc2014ebe5bd52264214fceaf212f6282f3250ba4b912092f29a964d84d85f839e9b6eb3496269ee810a1ccb1953ae9539c1cac60739217e92424e6716a030e56d518055c0d62c3bd54bdf8480e84490d34e2042598b46d7a1acd90046c8ea42d0b62ddbc194ef7cfcf8f9de6bb64253e1198884aa7dfa72385231c0b289368fd8bb6268ec3f6d401d7c384e574e09963f04b9dbfce3be3deeda41bfddb509567f0db5dbda64f0a65b50259f5ab546cbde229ebc55c05e0089207aff7e7f3712546dbfbdefc1479a0143a459a19dee87c35f000000ffff0300011a2ad8eb060000,2018-10-01 13:51:01+00,customer@gmail.com,,,CustomOrderId,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
|
||||
Id,Blob,Created,ExceptionStatus,Status,StoreDataId,Archived,Blob2
|
||||
Q7RqoHLngK9svM4MgRyi9y,\x1f8b0800000000000003c454cb76a24010dde72b7258cf180d08929d0a89899ae323ea4962163c0ae808ddd8344426c72f9bc57cd2fcc2340d31ea38b398cd2cfb5657d5add7fdf9fdc7fbd9f9b9845ce9ea5c1a6b9335e90db0dfd7936ca80cfd498ef45cfa52fc4818a1702bbec9893feb76d9ace3abd3d6e06e456dd76b2fece46eb8eee4d7032f9bae5f6fd44dbfb330ddd2995017a870c6691896f16200774442e4e41cae0b8c5a0cf8436dea6a4d56344d5135596e2ac286704690030f282abe349a724bd6e5a67c298cb089117746041fd815a5b2bb109304b1b6eb524812514347ef1bb2a6cd0686663e7a8b4779dcf3bd45eb5ae9bcb181e10f50c93ca6c44d1d768b3d422391817b172d2b2831880c489c229e0d4085478577890b7be51691823c418e1572d4b3c2043e60caabe29856ab578893520a58b4459a4d0d89a35bc1c54e73dec5534c84e5de8a8e520ad88c2c149ec0bb24c58ce6272c4f283e814e59399ddfe220762a48d5ebcb3f9b1a274ca380e08f24bbbaf9ec0c8b59fbdbc3d70965a20953566c8d2fbab589535b35dab61f78e9a4bb50c76b2af7bda9f18a56caca9ca1acbdf202e7a6375ccd4d50eca775539e8fbf69fa4a514c326e0e8d7870d7efb747fd66369faba38d366b4c641dc6c8c67660240b3d35c78dbe1be85dc38efcc9d7e7f832095ea4d38c1088457b5f4a9d87ee52ba5afe277a4b69fb71c1164b05270c6f5275370ec425e7cab6eb706ce511605660cf2fe575829762d7b243d85fe10a1e1e2e19475d44c161b3c9a0c818301627571717919530a0981f0767b342d8af39242ab9b0cd3588d3ad9762e0f150f784218f1f4d41b160c2685a26c57b8632c5a7b000cd80ce68789817610cace642446a36737875e5bf1aa17e99dfa179cc48b568d55df1c9ed1e7fd7a7f296cb9e0d8105a410bbf7edcee4014c4aefc60e3baa5860ffa454c279bbbb978860c4d59a77d7dce9e24e135bf54a130354487aa14855323898bf95f18916c3aeac3d2b090e7fc0860176c13d1ed2e76a40566dd0f1563d9010a88585f0356af5b3ed2f000000ffff030035140a5d88060000,2018-10-01 11:32:12+00,,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
|
||||
Ka6GHBrFJPRwFRga1RD6Yz,\x1f8b0800000000000003c454cd769a4014dee7297258b74682a066a7427e24e6284a729a9fc5001798083364188826c727eba28fd457e830101353db45375dcef7cdbdf7bbbf3fbfff783d383c5470a09c1c2a3632cece87ec743c759e4f9d08a98e697c7b51be543f724e195cc86f5a1eb9a31177879131ef5d8e97cc0bc2c18d978f274fc3f5e96558ce9f1ecf8c953dbcb182da98b2009834264592d4fe3280604a13ecaf05dc9618431cc44337ba46abad6aed76db50b5635d72989414fbb0c069f545d5b59ed6eff4f4da10561916c698921d5eef367c0019cd311f0401833c973998b7a5e742ef7110a7fd65e9f62889c7b6f5726b777d3fc25c9fd5ca334683c2e71724a42c9511847555b24a1287d484dcaffc9d310072b80024cd1a724403f89073e52e5ee7d84789404394e4f00633915a558656fbb881fc823120b2388ae53a8a4037529157ac452df7e991cc154a3fc594b095229cecc147b4209cadf730b738db83ce79dda3dffc60becf4953f1e33f53ea1e6a1a53f216649bb7e8a08938fa384362a870298b30e7d5ec44b25aabacf00c73e045715838a31b63f6c4343b9c9b8f78d9595a2e2e07cb30f6cfce27cb6b0b3adeed93ae5dcf5ebafd65a763d1993e31b3cbb16d0fa6b65e5e5f1bd355d7551dad0f33ec112f36f39b7e61cd543b88fb23d34b23e7eb5d769cc70fca7e4518e4b8bdde2bc3c5e85e39b9ff4ff2ee95cddb1e235e484d049e95667b7cc86acd0db7ad7086d629105e61770ff58e4258900079097c9ce1069eec0e994003ccc0e7ae7359458c39cff293a3a314e51c1811db21d42c31895a3e4d6b2d7c750a7281dbf5e686c2d515e538145b5349ac947056d441c907a20ef17e5e8095c05c96ecc6c584006f0590d296c77d915dfdaf455954c7f7d93ae3b419b466af44e7b68fbf5fa97a99eb9a4d80c7b43a79af9b2d150238b5b5bac53e652cb17fba57d278b3dd9794122c6eb6a8aeb5bd8edbcbd8d79acb18e3eab05727a909063bfd47a5e868d5ec863d4779bcfb03561c4800c1e726bd8f0694cd047d9eaa054d8021222f9fda6a1f6c7e010000ffff0300ddecc9e08e060000,2018-10-01 11:54:10+00,,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
|
||||
Q3kZ3F8cUD57WUqcc8QLs2,\x1f8b0800000000000003c4544972da4014ddfb1494d6099110a377806463a662b0c085f142c3176a23758b564b86b83859163952ae90564b96812259649365bff77ffff9fdfaf1f3fda654929023dd96a4a9ba5da9774ddbd06a8da5b1b3ede6741855a42fa945c408850761a6461ba3db654667539f3787fd2db51cb7bdb4a2fe68d739dc0ddd64be7bbdafef079da5ee64ce843a4085338e7d3ffb2f047026c447f681c3b2c0a8c9803f6af546bd2c2baa2ccb7545add404877042900d8f28484d949ada545bd566a32248d887883b2382cff85a23e71d08498458db71284491a861921876af3f1ec361bc5bb4d427dadb57bcfba145f4fd66fe36795a6599879438b1cd1eb04b68202270efb465694a0c020d223bfd6f64460c28260e94e6ccdc22bc11feb95597e327c5a7ff7a8708d9a6cf51d7f423f88029af916395b29c23764c2960d12449376612478f22332b3ef09e5ecb4b306333b80829603d30917f05ef9218337ab8c2ac507805e545b26bff7711bbf649def9ca9f29e50a35f108fe0852d4cd27a999cc3cdd25be5c28114d98b3748736a25bfb30b6ea5adbda786e3ceb2eebd31d5507ee5c7b45dbea563750d2deba9e7ddf1b6d173a54add5aea62ea6df1bad6db5aa93696da485c3fe60d09e0c6ac962519fec1b8632535b304516b63c2d5ab6627daa0c1cafd5d5ac6033fbfa1c5622ef45ba9e1102b176ef6ba9f3d85d4bb7ebff94de5a3a7edcb3c9629113863729bf221bc22ce79c2b3a1c9a8700304bb1e797ec56c18db1635a3e9cae700e8fce978ca30ea2603363364c237a8c85d1edb76f417134517633659b04592e6c7f07e290e54c1a5cfed59830e4f2a349534c336134ce82e213220bf129334013a006f5cfe3228c81951d0848d96236af2eb32b139addad64d343c848be68f95df1c9158fbfab5576cb59cf46c03c924adffbb1a05c8059e6ad14d845c502fb27dd12cec7e25e028211d76ede5dbd50c942215b6aae901e4a053e55a43c189ccddf4cf844d361e76ccf8cbc730bd833c00e389743fa5c0d48f20dbadcaa47e20335b1103ea52cdf1c7f030000ffff03003e6b8efb96060000,2018-10-01 11:54:32+00,,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
|
||||
FSktP1Nrxu7arh7TUFAgTZ,\x1f8b0800000000000003c4554b72e33610ddcf295c5c27b23e16297a1599923c63591efd55f1781620d1143124011a0035545c3a59163952ae10008415d9a54caab2c992af5f773ff48f7ffefec7cb878b0b8760e7fac2192d52396d3df0aaf4104fbce56ad4df2e1f9d9f344348c6e193a175c47615047275b37517bdfbbb948738ee6f42713779bed98feee3dde2f9dbad5b8d6f36435c3b338e81d7ce41a922e59f2d50872e00f0946524da2b46d3601c49501f5dd76b377a6dbfedf7bc96e7f9c646e88e91089624d79456b7d3ebf8dd66a76b8c501544391346dfda7d6bc7503041641f630e4298e70c969bd093de7886d63de1a2ea7b67ddc28fb76595dcfdba1db9eeb296597086cb487ea231e3b9c9a0bc75f5b42409f90044a4e34d9090c029c370b1902825746bfc2d2b50b862d132cb2c5a247b41229429344699805798ab376afdcd66a369b1a8e41ca82993335ccd1d851e8cb6b0dcab7a9e53662c0f287f97d4c0c31c119d56c5d54d01fe0b54282f3268442c774e99012ba9e4fb33311e497106550f97e73206449e0b62bbd1fe6753eb8c699a30fa9ae45809d5dd0192e884ae5acec9ce946521f55c6d4dfdaaa20cdd413fdc2671390f36eeec9977c6f162f08da457e9704576fd344ea2db8f93743d84abf0f1b9db59cf7ef3fcf4ea6ac866ddc9a0b8bf1b8ffbd37177b75ebbd3ca5bb5e61d1f6624a46132101bbf1cce5a639cf8c120ccb7f39fbf146d917c75ce2b226046f1e5c9b959064fcef5d3ff24efc939bcae3b92a5d144e1bb63372b82a2d66c6dc70a17689f03951afbf2b5de5f884b8a5198c1e9585b78f26f63a778987088e46a7eaf89899485b8bebc3ce15dca04d154ec59597bc86a04765dcc77acb43d304962b55a5ab4d6267959cba027861fa4504b9985284a85ad09f01df015cf4ef96a852805d9c090b3462823558a9ad760bc5e7c27e2fb42323b95762d559b8f1f3f3e77f531a80b3c0199307d465f0e47530c30afbd5b47ec5d310cf69f0e9f713e1c972b6794a8ff803a69c3e3993d9e58bf6b4f6c42f4cf429f349b0cde0c0bdaa9f6ebc9b0d68f48246f195049a018f0fbfefd3d47b0b3e3f67e04972c038e687d391bcd0f87bf000000ffff030075db901fe2060000,2018-10-01 11:57:15+00,,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
|
||||
HuzCsv9hghew2FD6zVqUyY,\x1f8b0800000000000003c455cb76d33010ddf3153d5e439aa75377456aa794268126a913286521dbe358b52d3992ecc6edc997b1e093f80564590d694f8073d8b0f49d3b3357f3f28f6fdf1f5f1d1d1938304e8f8c8bfcc1e68515ad22b86f9f3be6c362ed969f8dd715830bcae0bda275f8cab56de19eadccf9c9f832665e100e961ebf9caccfcaf37158ccd777efcccde86c390c6a67ca0260b5b39dcb48e9470dd4a13380e08a26d82f25a3a9308604c88f9ed96f374eda56db3ae9b7fa7d4bd9302928f6e11aa715a5d5eb9c74ac5edfea28236c322c9d3125cfeca6a9ed01649463310802069cabe758e9f2be701d7ad3b3379f9069a72be406a865ad3fdf59e36ceab46a9919a341ee8bf724a42c5519a47755bd4a9280d401ee57f126880b60840670341728c664a5fc35cb96b864913c49349a4525c73e4a241aa284c313cce41b2bfdcd66a3a9313f670c882a933174678644b74a9b9797b29e879429cb0794be48aae0618a709556c6ad9a02ec2d6c509a25d0f0696aec336d9a13c1ca03316e707600950f178732da581c0aa2bbd1febda975c0741551f294645709d95d0709b447972d67b85065998b6aae56aa7e9b2cf74c67e0ada2309fd94b73ba669d513877ee70dc8d872e2e067118f9ef2e26f162085def66ddeb2ca60f7d2bee768774da9b38d9f872341a5c8d7ac562615e6dfa6e6bd6b1608a3de2450e5f5af970da1a0591653b5eba9abdf992b579f4d538ac08831ac5c75be3ecdabe354e6fff93bc5b63fbb4ee48e44a13817b436f960f59ad59db7615ce5099021115f6e56bbdbf10e624405e02fb63ade1c9dfc64ef202ccc017ee6c5c112321327e7a7cbcc73b161122312f695e7b88cd39e87551dfa1d4f6810a1ccad5aa4457da04cb6b1964cff087147229130ff931d7350156007359b2cf972b4408884600296d78c297a5a8790dcaeac5377c566682eaa9d46b29dbbcfbf8f3b9ab8f415de00988885667f471bb338500b3dabbb5c35e144361ff74f894f376b75c292558fe07e4491beeceac3eb1dd66b3ad4f6c84ab9f4575d2743278362ca890edaf26435b2f108f9e336023800410bcecdfaf3982428fdbcb11bca6093044eacbd968bedafe040000ffff030069b828dae2060000,2018-10-01 12:09:53+00,,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
|
||||
YZV1PQasUKEXdSkSV69XDL,\x1f8b0800000000000003c455cb76db3610dde72b7cb86e65911429cbabca1463c792523d2c39759c05480c4584244003202bd5475fd6453e29bf50108455d9474dcfe9a64bdeb93373312f7efff3dbf3bbb3338b60ebf2ccfaed616dcfe648acc6e127bccc966b7ff06934b17e6a1842320e1f34cd159b5510c8d5d5c65f5e4c6e331ee164781f89dbe9d3d5eefd24a9974f5faffdedf8ea3ec4ad33e31878eb1c542a52f1ab01dad025009eb19cc43bc5e86a8c2309eac3f37da7e3f56cd7f55dcf751c6d23b46624863b523414db732fdc81ef38b636c2b624ca9930facadeb78d1d43c90491438c3908a19f33b85927f2b62f2807a7866c7ce1f5c49dbc76275e9a08e7a2ba6e65969ce12a961f68c278a13328efa67a8d2409c50844dcc49b2221815386e16c295146e846fb1b56a070c5a2559e1bb44c7782c42857688272012f30576f6cf477bb9daec1e28a73a0ba4c56b85a580add6b6d51b553f53ca54c5b3ea2e24d520d8705224d5a15b7690af05f608b8a32874ecc0aeb9819b08a4abe3b11e381942750f570792a6340e4a920a61bce3f9bec13a659cae84b92432554774748a223ba6a3927b52ecb523673b5d1f5db9655e48f86d1264daa4570efcf9fb83b4e96a3af24eb65e18ad4c32c49e3eb9b69b60ea1173d3c79ee7afe477f90f57a219b7bd35139b91d8f87b3b157afd7fe6cdb5fd90b77007312d1281d89fb4115ceed314e07c1282a368b9f3f978e48bf58a71511d0a3f8fc685ddd058fd6e5e3ff24efd1dabfac3b9295d644e177cb6c560c65abd9d80e152ed1ae002a1becf397767f21a92846510ec7636de0e9bf8d9de261c22196abc5a421a65296e2f2fcfc88772e534433b16355eb21b7efc1ac8bfe4e94b68f4c9244ad5623bad12679d5caa047861fa4504b994728ce84a909f01af88ae7c77cb5429482ec6028582792b12a45cbeb30de2ebe15f35d2999994ab396aacd878f1f9fbbf618b4059e824c5973469ff7075302b068bded03f6a6181afb4f874f3bef0fcb55304ad47f409db4f07066cd89ed75bb7d736253d2fc2c9a936692c1ab6141b56a7f3319c67a8344fa9a015b0914037edbbfbfe7086a336e6f47f08ee5c0116d2f67a7fb6eff17000000ffff03003a3b8e5ae2060000,2018-10-01 12:17:01+00,,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
|
||||
3qtdXmycQwhWEPs91MExB2,\x1f8b0800000000000003c455cb769b4810dde72b7c5827b21002455e4502f2b0a48c1e96e538cea2a10bd106ba5177238bf8e8cb66319f34bf90a6c18aeca324e7cc6696dcba5575bb5efcfbf73f8fafcece0c828d8b33c3da487c9395e1ec215ef953d13727fe6ed8315e570c2119874f354dac97ae2b97c3b5b3783bbe4c7880a3c12a109793cdb07c3f8eb68bcdfd0767371aae7c5c3b338e81d7ce6ea122657f35401d3a07c0539692b0548cb6c63892a03e6cc7e9b4ecae69598e655b9d8eb611ba6524842b925514d3b6de5a7dc7b11d6d845d4e943361f499bd6737760c3913440e30e620847e8ef9b0bcf70a6bf6ddc3e3e48bdfe38e7f39ef4d2837931b424641af969973868b507ea211e399cea0bcabea559224641e88b08a37414202a70cc3d942a284d0b5f66f58aec2158b1669daa0795c0a12a254a1114a053cc15cbd51eb373bad76038605e740759d0c7f393714bad7e282a254053d254d5b3ea3ec45560dfb1922555e15b7ea0af077b043599e422b649971cc745941252f4fc4b825f90954bd5c9ecae812792a48d38eceaf4de609d33466f429c9a112aabd1e92e888ae7acec956976521abc15aebfaedf22270bc41b08ea362eeae9cd9865ba368e1dd93a49bf84bb21d24511c7ef83849ae7de806b71bdbba9e7deff5936ed767337be2e5e3cbd168301dd9dbeb6b67baeb2dcdb9d587190968107b62d52ffc9939c271dff5826c3d7ff335ef88f89b715a11013d8b8f77c6f0cabd332eeefe277977c6fe69df912cb4260a0f46b35a21e4b5e6c676a8708eca0ca8acb0afdfea0586a8a01805291ccf75034ffe34768a870987502ee7e38a184b998b8bf3f323deb98c114d44c98ada43eede83de97767d4122a5ed33932452bb5589aeb4495ed432e891e13729d456a6010a13d1d404f816f892a7c77cb54294826c61c8582b90a12a45cd6b315e6fbe11f23297ac99ca662d559b0f1fbfbf77f535a80b3c0119b3ea8e3eee0fa608605e7b9b07ec453134f69f2e9f76de1f962b6394a81f81ba69fee1ce3637b6db36cde6c6c6a4fa5b5437ad4906cf86056d55fbabc968ac1f91889f3360278162c02ffbf7738e60db8cdbcb11bc62297044ebd3d96abfdaff000000ffff0300fd61e9cae3060000,2018-10-01 12:24:16+00,,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
|
||||
BtJGr7kt52QkZe8RdhKz42,\x1f8b0800000000000003c455c172da4810bde72b5c3a6f301208249f168462078cd780c115e21c469a161a4b9a9167465a888b2fcb219f945fc86824b3d8c566abf692a3de7b3dfda6a7bbf5e3dbf7e777676706c1c6c5993194e34bde4fa46dcd923538731c4fbe762de38f4a2124e3f051cb3a62b3f43cb91c6e7a0be77a9cf0004783fb408ca74fc3dd87eba85c3c3d5ef6b693e1bd8feb60c631f03ad82bd449d95f0d501f9d03e05b969270a7146d8d7124417dd83dc76a5996d9711dc771db3dcd115a3212c21dc92a8969779c8edb6ff72d4dc236272a9830fa9a771b1e43ce0491038c3908a1af73152d3ee55bfea9fb784978de21ebf286925537598ef12258737f52dbcc39c345283fd288f14c6750d155f52a4b12b21188b03a6f8a84044e1986b3854409a11b1ddfa83c852b152dd2b441f3782748885285462815f002737547eddf6cb79c7ebf6d9b96dd7061c139505d2ec35fce0d85eeb5c7a0d8a9ba9e72a8991b94bd49ae613f43a44a6fa4880b4168028efbe7a6025b21cb8c63a9c70a2af9eec4216b929f405505e4a9941e91a70e699ec5fa77ca3c41ddc68cbe243994423df308497424576fcf49a9ebb29055836d7401b77911f44683601347c5dcbbefcd9e7867122d468f24e926fe929483248ac3cbab69b2f2a11bac9fecce6af6b5ef26ddaecf66f674945f8f2793c1edc42e57abdeedb6bf34e71d176624a0413c12f76ee1cfcc098e5d6f14649bf9fbcfb925e22fc6694704744f3e3f18c33befc1b878f84df61e8cfdcbdc2359684f14fe369a110b21af3d37dca1c239da654065857dfe520f324405c52848e1b8bf1b78fa9f7da784987008e5727e5d2963297371717e0e5b94e52954ba7319239a881d2bea08b9fd007a70daf52a8994b91b2649a486ac725d9993bca87dd023e21729d478a6010a13d1140578097cc9d363bdf24d29c816868cb50219aa5ad4ba16e3f50a3042becb256bdab2194cf5ce878f5f2fbe7a2dd4159e828c59b5509ff7072a0298d7d1e6017b530c8dfdaf15a883f787e9ca1825ea8fa0969b7f58b8cdb2edb64dbb59b631a97e1bd5726b92c1ab6e41a57aeaaa351af60a89f8b502b6122806fcf6fdfe6924289b7e7bdb83772c058e68bd435bed77fb9f000000ffff030034f07389ec060000,2018-10-01 12:31:12+00,,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
|
||||
VWB3riH9WuCv9NtSedaGE6,\x1f8b0800000000000003c455c1729b4810bde72b5c9cb3b21012029f222162c792bc9664a494631f0668c404984133032bc5a52fcb219f945fc83060457629deaabdec91f75e4fbfe9e96e7e7efff1f4eeec4cc3a17671a62d574383e12b7b5538a57d231610a24bd7d4de570a2e28834f4a66f0b5e738c21baecd8535b94e981f468395cfafa79be1eee3242a179baf97e6763c5cb9611d4c5908ac0e760a7952f67703d447e700e12d4d71b0938ab6c21812203f7aa6d569753aba615b9665b74dc56152521cc01dce2a89de332cc3eeebb6ae48d8e65806634a5ef056bbe143c829c76210860c3857d7e96fd6b6037d4f1457c10adb9fbd9bfb8531b059d17326cbcbd5106a9b39a36111884f24a22c5319647455bdca92806c043ca8ce9b222e80111ac2d942a00493b58a6f548ec4a58a1469daa079bce33840a944239472788699bca3f2afb75b56bfdfeee99d5ec30505634054b934d79b6b12dd2b8f7eb193753de5503137287b955cc16e8670955e4b11e31c93042cfbc3ba025b01cdb463a9430b22d8eec421f7383f81ca0a8853291d2c4e1dd23c4be7cf947e82ba8d29794e7228857ce61112e8482edf9ee152d56521aa065bab026ef3c23747037f1d47c5dc5999b30d33c6d162f41527ddc4f5703948a238b8bc9a264b17bafefda6672c67dffa76d2edba74d69b8ef2c9f5783cb81df7cae5d2bcddf63d7d6ed830c33ef1e3115fd9853bd3c7616c3b233f5bcffffa927778fca89d768441f5e4d38336bc731eb48b87ffc9de83b67f9e7b240ae589c03f5a336201e4b5e7863b543847bb0c88a8b02f8ff52043549010f9291cf777034fffb5efa430c40c02e1cd2795321622e717e7e7b045599e42a53b17312209dfd1a28e10db8fa006a75daf92489abba1024772c82ad79539c18ada073922de4821c733f55190f0a628c04a601e4b8ff5d23721205a2164b4e58b40d6a2d6b528ab578016b05d2e68d396cd60ca773e7cbcbdf8eab55057780a22a6d5427dda1fa808605e47eb07ec553114f69f56a00ade1fa62ba304cb3f825c6eee61e136cbb6dbd6cd66d9c6b8fa6d54cbad49062fba0595f2a9abd668d82bc4e3970ad80a202184afdfef772341d9f4dbeb1ebca3293044ea1dda6abfdbff020000ffff0300e1a26ae9ec060000,2018-10-01 12:33:11+00,,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
|
||||
At4CDM5vfewV2WDHEPKRbt,\x1f8b0800000000000003c4554d73a33810bdcfaf4871de758c0db6c9696d20c9c471c61fb15d35933908688c06908824183329ffb23dec4fdabfb092205e27e59daddacb1e79efb5faa9d5ddfcf9fb1f2f1f2e2e0c1c195717c65858ae37b3ab18be6f7a5befd69f4f9781307e510a2e28838f5ad6e7bbb5eb8af56437588deeef521644f1781bf0bbd9f3a4bebe8fabd5f3b79bc17e3ad9fa51134c5904ac09764b7952fea9059aa30b80684e331cd652d1d5184302e4873d18f53b3db33f1c398edd1d3a9ac3a4a23884479c2b8969f74756d71c0d4c4dc2bec0321853f286ef0d5b3e8282722cc651c480737d9db1fde361b901320bf8c4ab83bb38c2dc22d73dd7de06f5eac6228dcd82d1a80cc547125396eb0c325a554f5912907bc04375de0c71018cd0082e5602a598ec747cab72252e55a4ccb2162d929ae31065128d51c6e11566f28ecabf351a76468ee90c7bc3960a4bc680e86a19fe7a6948f4a02d06652dcb7acea0661e50fe2eb786fd1c6195ddc810e31c931446ce6f3b0576429a1ba752979644b0facc219f717106950510e752ba589c3ba47d95de3f53e6196a9e50f29ae4580af9ca1e12e8442e9f9ee14ad76525547fed7401f745190cbc71b04be272e96e078b67d69fc62bef1b4eadd45fe36a9cc64978733b4b373e58c1e767bbbf59fc183aa965f97461cfbce2fe6e3a1dcfa776b5d90ce6fbe1da5cf61d58e0800489c7b74ee92fcc699438ae17e4bbe5af5f8a1e4fbe1ae71d61d02df9f2644c1edd27e3eae97fb2f7641c5ec71e89527b22f0dd68272c84a2f1dc72c70a17a8ce8108857df9dacc31c425895090c1697bb7f0ec5ffb4e0a23cc2014ebe5bd52264214fceaf212f6282f3250ba4b912092f29a964d84d85f839e9b6eb3496269ee810a1ccb1953ae9539c1cac60739217e92424e6716a030e56d518055c0d62c3bd54bdf8480e84490d34e2042598b46d7a1acd90046c8ea42d0b62ddbc194ef7cfcf8f9de6bb64253e1198884aa7dfa72385231c0b289368fd8bb6268ec3f6d401d7c384e574e09963f04b9dbfce3be3deeda41bfddb509567f0db5dbda64f0a65b50259f5ab546cbde229ebc55c05e0089207aff7e7f3712546dbfbdefc1479a0143a459a19dee87c35f000000ffff0300011a2ad8eb060000,2018-10-01 13:51:01+00,,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
|
||||
|
|
@@ -133,7 +133,7 @@ namespace BTCPayServer.Tests
|
||||
public async Task CanQueryDirectProviders()
|
||||
{
|
||||
// TODO: Check once in a while whether or not they are working again
|
||||
string[] brokenShitcoinCasinos = { };
|
||||
string[] brokenShitcoinCasinos = { "binance", "coinbasepro" };
|
||||
var skipped = 0;
|
||||
var factory = FastTests.CreateBTCPayRateFactory();
|
||||
var directlySupported = factory.AvailableRateProviders.Where(s => s.Source == RateSource.Direct)
|
||||
@@ -272,7 +272,8 @@ namespace BTCPayServer.Tests
|
||||
"https://www.bitpay.com", // not allowing to be hit from circleci
|
||||
"https://support.bitpay.com",
|
||||
"https://www.coingecko.com", // unhappy service
|
||||
"https://www.wasabiwallet.io" // Banning US, CI unhappy
|
||||
"https://www.wasabiwallet.io", // Banning US, CI unhappy
|
||||
"https://fullynoded.app" // Sometimes DNS doesn't work
|
||||
};
|
||||
|
||||
foreach (var match in regex.Matches(text).OfType<Match>())
|
||||
|
@@ -1448,6 +1448,41 @@ namespace BTCPayServer.Tests
|
||||
Assert.Contains("DOGE_X", rateVm.Script, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanTopUpPullPayment()
|
||||
{
|
||||
using var tester = CreateServerTester();
|
||||
await tester.StartAsync();
|
||||
var user = tester.NewAccount();
|
||||
await user.GrantAccessAsync(true);
|
||||
await user.RegisterDerivationSchemeAsync("BTC");
|
||||
var client = await user.CreateClient();
|
||||
var pp = await client.CreatePullPayment(user.StoreId, new()
|
||||
{
|
||||
Currency = "BTC",
|
||||
Amount = 1.0m,
|
||||
PaymentMethods = [ "BTC-CHAIN" ]
|
||||
});
|
||||
var controller = user.GetController<UIInvoiceController>();
|
||||
var invoice = await controller.CreateInvoiceCoreRaw(new()
|
||||
{
|
||||
Amount = 0.5m,
|
||||
Currency = "BTC",
|
||||
}, controller.HttpContext.GetStoreData(), controller.Url.Link(null, null), [PullPaymentHostedService.GetInternalTag(pp.Id)]);
|
||||
await client.MarkInvoiceStatus(user.StoreId, invoice.Id, new() { Status = InvoiceStatus.Settled });
|
||||
|
||||
await TestUtils.EventuallyAsync(async () =>
|
||||
{
|
||||
var payouts = await client.GetPayouts(pp.Id);
|
||||
var payout = Assert.Single(payouts);
|
||||
Assert.Equal("TOPUP", payout.PayoutMethodId);
|
||||
Assert.Equal(invoice.Id, payout.Destination);
|
||||
Assert.Equal(-0.5m, payout.Amount);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanUseDefaultCurrency()
|
||||
@@ -2091,7 +2126,7 @@ namespace BTCPayServer.Tests
|
||||
Amount = 0.0001m,
|
||||
Destination = (await tester.ExplorerNode.GetNewAddressAsync()).ToString(),
|
||||
Approved = true,
|
||||
PaymentMethod = "BTC"
|
||||
PayoutMethodId = "BTC"
|
||||
});
|
||||
await user.AssertHasWebhookEvent(WebhookEventType.PayoutCreated, (WebhookPayoutEvent x)=> Assert.Equal(payout.Id, x.PayoutId));
|
||||
await client.MarkPayout(user.StoreId, payout.Id, new MarkPayoutRequest(){ State = PayoutState.AwaitingApproval});
|
||||
@@ -2414,9 +2449,10 @@ namespace BTCPayServer.Tests
|
||||
private static bool IsMapped(Invoice invoice, ApplicationDbContext ctx)
|
||||
{
|
||||
var h = BitcoinAddress.Create(invoice.BitcoinAddress, Network.RegTest).ScriptPubKey.Hash.ToString();
|
||||
var pmi = PaymentTypes.CHAIN.GetPaymentMethodId("BTC");
|
||||
return (ctx.AddressInvoices.Where(i => i.InvoiceDataId == invoice.Id).ToArrayAsync().GetAwaiter()
|
||||
.GetResult())
|
||||
.Where(i => i.GetAddress() == h).Any();
|
||||
.Where(i => i.Address == h && i.PaymentMethodId == pmi.ToString()).Any();
|
||||
}
|
||||
|
||||
|
||||
@@ -2800,7 +2836,6 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal("coingecko", b.PreferredExchange);
|
||||
}
|
||||
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanDoInvoiceMigrations()
|
||||
@@ -3122,7 +3157,15 @@ namespace BTCPayServer.Tests
|
||||
var invoiceId = GetInvoiceId(resp);
|
||||
await acc.PayOnChain(invoiceId);
|
||||
|
||||
app = await client.CreatePointOfSaleApp(acc.StoreId, new PointOfSaleAppRequest
|
||||
// Quick unrelated test on GetMonitoredInvoices
|
||||
var invoiceRepo = tester.PayTester.GetService<InvoiceRepository>();
|
||||
var monitored = Assert.Single(await invoiceRepo.GetMonitoredInvoices(PaymentMethodId.Parse("BTC-CHAIN")), i => i.Id == invoiceId);
|
||||
Assert.Single(monitored.Payments);
|
||||
monitored = Assert.Single(await invoiceRepo.GetMonitoredInvoices(PaymentMethodId.Parse("BTC-CHAIN"), true), i => i.Id == invoiceId);
|
||||
Assert.Single(monitored.Payments);
|
||||
//
|
||||
|
||||
app = await client.CreatePointOfSaleApp(acc.StoreId, new PointOfSaleAppRequest
|
||||
{
|
||||
AppName = "Cart",
|
||||
DefaultView = PosViewType.Cart,
|
||||
@@ -3210,7 +3253,7 @@ namespace BTCPayServer.Tests
|
||||
var inv = await client.CreateInvoice(acc.StoreId, new CreateInvoiceRequest() { Amount = 10m, Currency = "USD" });
|
||||
await acc.PayInvoice(inv.Id);
|
||||
await client.MarkInvoiceStatus(acc.StoreId, inv.Id, new MarkInvoiceStatusRequest() { Status = InvoiceStatus.Settled });
|
||||
var refund = await client.RefundInvoice(acc.StoreId, inv.Id, new RefundInvoiceRequest() { RefundVariant = RefundVariant.Fiat, PaymentMethod = "BTC-CHAIN" });
|
||||
var refund = await client.RefundInvoice(acc.StoreId, inv.Id, new RefundInvoiceRequest() { RefundVariant = RefundVariant.Fiat, PayoutMethodId = "BTC-CHAIN" });
|
||||
|
||||
async Task AssertData(string currency, decimal awaiting, decimal limit, decimal completed, bool fullyPaid)
|
||||
{
|
||||
@@ -3229,7 +3272,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
|
||||
await AssertData("USD", awaiting: 0.0m, limit: 10.0m, completed: 0.0m, fullyPaid: false);
|
||||
var payout = await client.CreatePayout(refund.Id, new CreatePayoutRequest() { Destination = addr.ToString(), PaymentMethod = "BTC-CHAIN" });
|
||||
var payout = await client.CreatePayout(refund.Id, new CreatePayoutRequest() { Destination = addr.ToString(), PayoutMethodId = "BTC-CHAIN" });
|
||||
await AssertData("USD", awaiting: 10.0m, limit: 10.0m, completed: 0.0m, fullyPaid: false);
|
||||
await client.ApprovePayout(acc.StoreId, payout.Id, new ApprovePayoutRequest());
|
||||
await AssertData("USD", awaiting: 10.0m, limit: 10.0m, completed: 0.0m, fullyPaid: false);
|
||||
|
@@ -29,6 +29,11 @@ namespace BTCPayServer.Tests
|
||||
BTCPayLogs.Configure(LoggerFactory);
|
||||
}
|
||||
|
||||
public DatabaseTester CreateDBTester()
|
||||
{
|
||||
return new DatabaseTester(TestLogs, LoggerFactory);
|
||||
}
|
||||
|
||||
public BTCPayNetworkProvider CreateNetworkProvider(ChainName chainName)
|
||||
{
|
||||
var conf = new ConfigurationRoot(new List<IConfigurationProvider>()
|
||||
@@ -58,9 +63,9 @@ namespace BTCPayServer.Tests
|
||||
var bootstrap = Startup.CreateBootstrap(conf);
|
||||
var services = new PluginServiceCollection(new ServiceCollection(), bootstrap);
|
||||
var plugins = new List<BaseBTCPayServerPlugin>() { new BitcoinPlugin() };
|
||||
#if ALTCOINS
|
||||
|
||||
plugins.Add(new BTCPayServer.Plugins.Altcoins.AltcoinsPlugin());
|
||||
#endif
|
||||
|
||||
foreach (var p in plugins)
|
||||
{
|
||||
p.Execute(services);
|
||||
|
@@ -321,7 +321,7 @@ retry:
|
||||
base.VisitTagHelper(node);
|
||||
}
|
||||
|
||||
private string ToString(IntermediateNode? node)
|
||||
private string ToString(IntermediateNode node)
|
||||
{
|
||||
return _txt.Substring(node.Source.Value.AbsoluteIndex, node.Source.Value.Length);
|
||||
}
|
||||
|
@@ -10,14 +10,12 @@ services:
|
||||
context: ..
|
||||
dockerfile: BTCPayServer.Tests/Dockerfile
|
||||
args:
|
||||
CONFIGURATION_NAME: Altcoins-Release
|
||||
CONFIGURATION_NAME: Release
|
||||
environment:
|
||||
TESTS_EXPERIMENTALV2_CONFIRM: "true"
|
||||
TESTS_BTCRPCCONNECTION: server=http://bitcoind:43782;ceiwHEbqWI83:DwubwWsoo3
|
||||
TESTS_LTCRPCCONNECTION: server=http://litecoind:43782;ceiwHEbqWI83:DwubwWsoo3
|
||||
TESTS_BTCNBXPLORERURL: http://nbxplorer:32838/
|
||||
TESTS_LTCNBXPLORERURL: http://nbxplorer:32838/
|
||||
TESTS_DB: "Postgres"
|
||||
TESTS_POSTGRES: User ID=postgres;Include Error Detail=true;Host=postgres;Port=5432;Database=btcpayserver
|
||||
TESTS_EXPLORER_POSTGRES: User ID=postgres;Include Error Detail=true;Host=postgres;Port=5432;Database=nbxplorer
|
||||
TESTS_HOSTNAME: tests
|
||||
|
@@ -12,10 +12,8 @@ services:
|
||||
args:
|
||||
CONFIGURATION_NAME: Release
|
||||
environment:
|
||||
TESTS_EXPERIMENTALV2_CONFIRM: "true"
|
||||
TESTS_BTCRPCCONNECTION: server=http://bitcoind:43782;ceiwHEbqWI83:DwubwWsoo3
|
||||
TESTS_BTCNBXPLORERURL: http://nbxplorer:32838/
|
||||
TESTS_DB: "Postgres"
|
||||
TESTS_POSTGRES: User ID=postgres;Include Error Detail=true;Host=postgres;Port=5432;Database=btcpayserver
|
||||
TESTS_EXPLORER_POSTGRES: User ID=postgres;Include Error Detail=true;Host=postgres;Port=5432;Database=nbxplorer
|
||||
TESTS_HOSTNAME: tests
|
||||
@@ -168,7 +166,6 @@ services:
|
||||
dev-bitcoind-poll=1
|
||||
ports:
|
||||
- "30992:9835" # api port
|
||||
- "30892:9735" # server port
|
||||
expose:
|
||||
- "9735" # server port
|
||||
- "9835" # api port
|
||||
@@ -197,7 +194,6 @@ services:
|
||||
dev-bitcoind-poll=1
|
||||
ports:
|
||||
- "30993:9835" # api port
|
||||
- "30893:9735" # server port
|
||||
expose:
|
||||
- "9735" # server port
|
||||
- "9835" # api port
|
||||
@@ -243,7 +239,6 @@ services:
|
||||
ports:
|
||||
- "35531:8080"
|
||||
- "53280:10009"
|
||||
- "30894:9735"
|
||||
expose:
|
||||
- "8080"
|
||||
- "9735"
|
||||
@@ -280,7 +275,6 @@ services:
|
||||
no-rest-tls=1
|
||||
ports:
|
||||
- "35532:8080"
|
||||
- "30895:9735"
|
||||
expose:
|
||||
- "8080"
|
||||
- "9735"
|
||||
|
@@ -83,7 +83,7 @@ curl -s -k -X PUT -H 'Content-Type: application/json' \
|
||||
# Fund Satoshis Steaks wallet
|
||||
btcaddress_satoshis_steaks=$(curl -s -k -X GET -H 'Content-Type: application/json' \
|
||||
-H "Authorization: token $admin_api_key" \
|
||||
"$API_BASE/stores/$store_id_satoshis_steaks/payment-methods/onchain/BTC/wallet/address" | jq -r '.address')
|
||||
"$API_BASE/stores/$store_id_satoshis_steaks/payment-methods/BTC-CHAIN/wallet/address" | jq -r '.address')
|
||||
|
||||
./docker-bitcoin-cli.sh sendtoaddress "$btcaddress_satoshis_steaks" 6.15 >/dev/null 2>&1
|
||||
|
||||
@@ -167,7 +167,7 @@ printf "Nakamoto Nuggets Cart POS ID: %s\n" "$cart_app_id_nakamoto_nuggets"
|
||||
# Fund Nakamoto Nuggets wallet
|
||||
btcaddress_nakamoto_nuggets=$(curl -s -k -X GET -H 'Content-Type: application/json' \
|
||||
-H "Authorization: token $admin_api_key" \
|
||||
"$API_BASE/stores/$store_id_nakamoto_nuggets/payment-methods/onchain/BTC/wallet/address" | jq -r '.address')
|
||||
"$API_BASE/stores/$store_id_nakamoto_nuggets/payment-methods/BTC-CHAIN/wallet/address" | jq -r '.address')
|
||||
|
||||
./docker-bitcoin-cli.sh sendtoaddress "$btcaddress_nakamoto_nuggets" 6.15 >/dev/null 2>&1
|
||||
|
||||
|
@@ -1,357 +0,0 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayApp.CommonServer;
|
||||
using BTCPayApp.CommonServer.Models;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Plugins.PointOfSale;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using Microsoft.AspNetCore.Authentication.BearerToken;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Identity.Data;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NicolasDorier.RateLimits;
|
||||
using PosViewType = BTCPayServer.Plugins.PointOfSale.PosViewType;
|
||||
using SignInResult = Microsoft.AspNetCore.Identity.SignInResult;
|
||||
|
||||
namespace BTCPayServer.App.API;
|
||||
|
||||
public partial class AppApiController
|
||||
{
|
||||
private const string Scheme = AuthenticationSchemes.GreenfieldBearer;
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpPost("register")]
|
||||
[RateLimitsFilter(ZoneLimits.Login, Scope = RateLimitsScope.RemoteAddress)]
|
||||
public async Task<Results<Ok<AccessTokenResponse>, Ok<SignupResult>, EmptyHttpResult, ProblemHttpResult>> Register(SignupRequest signup)
|
||||
{
|
||||
var policies = await settingsRepository.GetSettingAsync<PoliciesSettings>() ?? new PoliciesSettings();
|
||||
if (policies.LockSubscription)
|
||||
return TypedResults.Problem("This instance does not allow public user registration", statusCode: 401);
|
||||
|
||||
var errorMessage = "Invalid signup attempt.";
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
var isFirstAdmin = !(await userManager.GetUsersInRoleAsync(Roles.ServerAdmin)).Any();
|
||||
var user = new ApplicationUser
|
||||
{
|
||||
UserName = signup.Email,
|
||||
Email = signup.Email,
|
||||
RequiresEmailConfirmation = policies.RequiresConfirmedEmail,
|
||||
RequiresApproval = policies.RequiresUserApproval,
|
||||
Created = DateTimeOffset.UtcNow,
|
||||
Approved = isFirstAdmin // auto-approve first admin and users created by an admin
|
||||
};
|
||||
|
||||
var result = await userManager.CreateAsync(user, signup.Password);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
if (isFirstAdmin)
|
||||
{
|
||||
await roleManager.CreateAsync(new IdentityRole(Roles.ServerAdmin));
|
||||
await userManager.AddToRoleAsync(user, Roles.ServerAdmin);
|
||||
var settings = await settingsRepository.GetSettingAsync<ThemeSettings>() ?? new ThemeSettings();
|
||||
if (settings.FirstRun)
|
||||
{
|
||||
settings.FirstRun = false;
|
||||
await settingsRepository.UpdateSetting(settings);
|
||||
}
|
||||
|
||||
await settingsRepository.FirstAdminRegistered(policies, btcpayOptions.UpdateUrl != null, btcpayOptions.DisableRegistration, logs);
|
||||
}
|
||||
|
||||
eventAggregator.Publish(new UserRegisteredEvent
|
||||
{
|
||||
RequestUri = Request.GetAbsoluteRootUri(),
|
||||
User = user,
|
||||
Admin = isFirstAdmin
|
||||
});
|
||||
|
||||
SignInResult? signInResult = null;
|
||||
var requiresConfirmedEmail = policies.RequiresConfirmedEmail && !user.EmailConfirmed;
|
||||
var requiresUserApproval = policies.RequiresUserApproval && !user.Approved;
|
||||
if (!requiresConfirmedEmail && !requiresUserApproval)
|
||||
{
|
||||
signInManager.AuthenticationScheme = Scheme;
|
||||
signInResult = await signInManager.PasswordSignInAsync(signup.Email, signup.Password, true, true);
|
||||
}
|
||||
|
||||
if (signInResult?.Succeeded is true)
|
||||
{
|
||||
_logger.LogInformation("User {Email} logged in", user.Email);
|
||||
return TypedResults.Empty;
|
||||
}
|
||||
var response = new SignupResult
|
||||
{
|
||||
Email = user.Email,
|
||||
RequiresConfirmedEmail = requiresConfirmedEmail,
|
||||
RequiresUserApproval = requiresUserApproval
|
||||
};
|
||||
return TypedResults.Ok(response);
|
||||
}
|
||||
errorMessage = result.ToString();
|
||||
}
|
||||
|
||||
return TypedResults.Problem(errorMessage, statusCode: 401);
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpPost("login")]
|
||||
[RateLimitsFilter(ZoneLimits.Login, Scope = RateLimitsScope.RemoteAddress)]
|
||||
public async Task<Results<Ok<AccessTokenResponse>, EmptyHttpResult, ProblemHttpResult>> Login(LoginRequest login)
|
||||
{
|
||||
var errorMessage = "Invalid login attempt.";
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
// Require the user to pass basic checks (approval, confirmed email, not disabled) before they can log on
|
||||
var user = await userManager.FindByEmailAsync(login.Email);
|
||||
if (!UserService.TryCanLogin(user, out var message))
|
||||
{
|
||||
return TypedResults.Problem(message, statusCode: 401);
|
||||
}
|
||||
|
||||
signInManager.AuthenticationScheme = Scheme;
|
||||
var signInResult = await signInManager.PasswordSignInAsync(login.Email, login.Password, true, true);
|
||||
if (signInResult.RequiresTwoFactor)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(login.TwoFactorCode))
|
||||
signInResult = await signInManager.TwoFactorAuthenticatorSignInAsync(login.TwoFactorCode, true, true);
|
||||
else if (!string.IsNullOrEmpty(login.TwoFactorRecoveryCode))
|
||||
signInResult = await signInManager.TwoFactorRecoveryCodeSignInAsync(login.TwoFactorRecoveryCode);
|
||||
}
|
||||
|
||||
// TODO: Add FIDO and LNURL Auth
|
||||
|
||||
if (signInResult.IsLockedOut)
|
||||
{
|
||||
_logger.LogWarning("User {Email} tried to log in, but is locked out", user.Email);
|
||||
}
|
||||
else if (signInResult.Succeeded)
|
||||
{
|
||||
_logger.LogInformation("User {Email} logged in", user.Email);
|
||||
return TypedResults.Empty;
|
||||
}
|
||||
|
||||
errorMessage = signInResult.ToString();
|
||||
}
|
||||
|
||||
return TypedResults.Problem(errorMessage, statusCode: 401);
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpPost("login/code")]
|
||||
[RateLimitsFilter(ZoneLimits.Login, Scope = RateLimitsScope.RemoteAddress)]
|
||||
public async Task<Results<Ok<AccessTokenResponse>, EmptyHttpResult, ProblemHttpResult>> LoginWithCode([FromBody] string loginCode)
|
||||
{
|
||||
const string errorMessage = "Invalid login attempt.";
|
||||
if (!string.IsNullOrEmpty(loginCode))
|
||||
{
|
||||
var code = loginCode.Split(';').First();
|
||||
var userId = userLoginCodeService.Verify(code);
|
||||
var user = userId is null ? null : await userManager.FindByIdAsync(userId);
|
||||
if (!UserService.TryCanLogin(user, out var message))
|
||||
{
|
||||
return TypedResults.Problem(message, statusCode: 401);
|
||||
}
|
||||
|
||||
signInManager.AuthenticationScheme = Scheme;
|
||||
await signInManager.SignInAsync(user, false, "LoginCode");
|
||||
|
||||
_logger.LogInformation("User {Email} logged in with a login code", user.Email);
|
||||
return TypedResults.Empty;
|
||||
}
|
||||
|
||||
return TypedResults.Problem(errorMessage, statusCode: 401);
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpPost("refresh")]
|
||||
[RateLimitsFilter(ZoneLimits.Login, Scope = RateLimitsScope.RemoteAddress)]
|
||||
public async Task<Results<Ok<AccessTokenResponse>, UnauthorizedHttpResult, SignInHttpResult, ChallengeHttpResult>> Refresh(RefreshRequest refresh)
|
||||
{
|
||||
var authenticationTicket = bearerTokenOptions.Get(Scheme).RefreshTokenProtector.Unprotect(refresh.RefreshToken);
|
||||
var expiresUtc = authenticationTicket?.Properties.ExpiresUtc;
|
||||
|
||||
ApplicationUser? user = null;
|
||||
int num;
|
||||
if (expiresUtc.HasValue)
|
||||
{
|
||||
DateTimeOffset valueOrDefault = expiresUtc.GetValueOrDefault();
|
||||
num = timeProvider.GetUtcNow() >= valueOrDefault ? 1 : 0;
|
||||
}
|
||||
else
|
||||
num = 1;
|
||||
bool flag = num != 0;
|
||||
if (!flag)
|
||||
{
|
||||
signInManager.AuthenticationScheme = Scheme;
|
||||
user = await signInManager.ValidateSecurityStampAsync(authenticationTicket?.Principal);
|
||||
}
|
||||
|
||||
return user != null
|
||||
? TypedResults.SignIn(await signInManager.CreateUserPrincipalAsync(user), authenticationScheme: Scheme)
|
||||
: TypedResults.Challenge(authenticationSchemes: new[] { Scheme });
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpPost("accept-invite")]
|
||||
[RateLimitsFilter(ZoneLimits.Login, Scope = RateLimitsScope.RemoteAddress)]
|
||||
public async Task<IActionResult> AcceptInvite(AcceptInviteRequest invite)
|
||||
{
|
||||
if (string.IsNullOrEmpty(invite.UserId) || string.IsNullOrEmpty(invite.Code))
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var user = await userManager.FindByInvitationTokenAsync(invite.UserId, Uri.UnescapeDataString(invite.Code));
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var requiresEmailConfirmation = user is { RequiresEmailConfirmation: true, EmailConfirmed: false };
|
||||
var requiresUserApproval = user is { RequiresApproval: true, Approved: false };
|
||||
bool? emailHasBeenConfirmed = requiresEmailConfirmation ? false : null;
|
||||
var requiresSetPassword = !await userManager.HasPasswordAsync(user);
|
||||
string? passwordSetCode = requiresSetPassword ? await userManager.GeneratePasswordResetTokenAsync(user) : null;
|
||||
|
||||
eventAggregator.Publish(new UserInviteAcceptedEvent
|
||||
{
|
||||
User = user,
|
||||
RequestUri = Request.GetAbsoluteRootUri()
|
||||
});
|
||||
|
||||
if (requiresEmailConfirmation)
|
||||
{
|
||||
var emailConfirmCode = await userManager.GenerateEmailConfirmationTokenAsync(user);
|
||||
var result = await userManager.ConfirmEmailAsync(user, emailConfirmCode);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
emailHasBeenConfirmed = true;
|
||||
eventAggregator.Publish(new UserConfirmedEmailEvent
|
||||
{
|
||||
User = user,
|
||||
RequestUri = Request.GetAbsoluteRootUri()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
var response = new AcceptInviteResult(user.Email!)
|
||||
{
|
||||
EmailHasBeenConfirmed = emailHasBeenConfirmed,
|
||||
RequiresUserApproval = requiresUserApproval,
|
||||
PasswordSetCode = passwordSetCode
|
||||
};
|
||||
return Ok(response);
|
||||
}
|
||||
|
||||
[HttpPost("logout")]
|
||||
public async Task<IResult> Logout()
|
||||
{
|
||||
var user = await userManager.GetUserAsync(User);
|
||||
if (user != null)
|
||||
{
|
||||
await signInManager.SignOutAsync();
|
||||
_logger.LogInformation("User {Email} logged out", user.Email);
|
||||
return Results.Ok();
|
||||
}
|
||||
return Results.Unauthorized();
|
||||
}
|
||||
|
||||
[HttpGet("user")]
|
||||
public async Task<IActionResult> UserInfo()
|
||||
{
|
||||
var user = await userManager.GetUserAsync(User);
|
||||
if (user == null) return NotFound();
|
||||
|
||||
var userStores = await storeRepository.GetStoresByUserId(user.Id);
|
||||
var stores = new List<AppUserStoreInfo>();
|
||||
foreach (var store in userStores)
|
||||
{
|
||||
var userStore = store.UserStores.Find(us => us.ApplicationUserId == user.Id && us.StoreDataId == store.Id)!;
|
||||
var apps = await appService.GetAllApps(user.Id, false, store.Id);
|
||||
var posApp = apps.FirstOrDefault(app => app.AppType == PointOfSaleAppType.AppType && app.App.GetSettings<PointOfSaleSettings>().DefaultView == PosViewType.Light);
|
||||
var storeBlob = userStore.StoreData.GetStoreBlob();
|
||||
stores.Add(new AppUserStoreInfo
|
||||
{
|
||||
Id = store.Id,
|
||||
Name = store.StoreName,
|
||||
Archived = store.Archived,
|
||||
RoleId = userStore.StoreRole.Id,
|
||||
PosAppId = posApp?.Id,
|
||||
DefaultCurrency = storeBlob.DefaultCurrency,
|
||||
Permissions = userStore.StoreRole.Permissions,
|
||||
LogoUrl = storeBlob.LogoUrl != null
|
||||
? await uriResolver.Resolve(Request.GetAbsoluteRootUri(), storeBlob.LogoUrl)
|
||||
: null,
|
||||
});
|
||||
}
|
||||
|
||||
var userBlob = user.GetBlob();
|
||||
var info = new AppUserInfo
|
||||
{
|
||||
UserId = user.Id,
|
||||
Name = userBlob?.Name,
|
||||
ImageUrl = !string.IsNullOrEmpty(userBlob?.ImageUrl)
|
||||
? await uriResolver.Resolve(Request.GetAbsoluteRootUri(), UnresolvedUri.Create(userBlob.ImageUrl))
|
||||
: null,
|
||||
Email = await userManager.GetEmailAsync(user),
|
||||
Roles = await userManager.GetRolesAsync(user),
|
||||
Stores = stores
|
||||
};
|
||||
return Ok(info);
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpPost("forgot-password")]
|
||||
[RateLimitsFilter(ZoneLimits.ForgotPassword, Scope = RateLimitsScope.RemoteAddress)]
|
||||
public async Task<IResult> ForgotPassword(ResetPasswordRequest resetRequest)
|
||||
{
|
||||
var user = await userManager.FindByEmailAsync(resetRequest.Email);
|
||||
if (UserService.TryCanLogin(user, out _))
|
||||
{
|
||||
eventAggregator.Publish(new UserPasswordResetRequestedEvent
|
||||
{
|
||||
User = user,
|
||||
RequestUri = Request.GetAbsoluteRootUri()
|
||||
});
|
||||
}
|
||||
return TypedResults.Ok();
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpPost("reset-password")]
|
||||
public async Task<IActionResult> SetPassword(ResetPasswordRequest resetRequest)
|
||||
{
|
||||
var user = await userManager.FindByEmailAsync(resetRequest.Email);
|
||||
var needsInitialPassword = user != null && !await userManager.HasPasswordAsync(user);
|
||||
// Let unapproved users set a password. Otherwise, don't reveal that the user does not exist.
|
||||
if (!UserService.TryCanLogin(user, out var message) && !needsInitialPassword || user == null)
|
||||
{
|
||||
_logger.LogWarning("User {Email} tried to reset password, but failed: {Message}", user?.Email ?? "(NO EMAIL)", message);
|
||||
return Unauthorized(new GreenfieldAPIError(null, "Invalid request"));
|
||||
}
|
||||
|
||||
IdentityResult result;
|
||||
try
|
||||
{
|
||||
result = await userManager.ResetPasswordAsync(user, resetRequest.ResetCode, resetRequest.NewPassword);
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
result = IdentityResult.Failed(userManager.ErrorDescriber.InvalidToken());
|
||||
}
|
||||
return result.Succeeded ? Ok() : this.CreateAPIError(401, "unauthorized", result.ToString().Split(": ").Last());
|
||||
}
|
||||
}
|
@@ -1,32 +0,0 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayApp.CommonServer.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.App.API;
|
||||
|
||||
public partial class AppApiController
|
||||
{
|
||||
[HttpGet("create-store")]
|
||||
public async Task<IActionResult> CreateStore()
|
||||
{
|
||||
var defaultCurrency = (await settingsRepository.GetSettingAsync<PoliciesSettings>())?.DefaultCurrency ?? StoreBlob.StandardDefaultCurrency;
|
||||
var defaultExchange = defaultRules.GetRecommendedExchange(defaultCurrency);
|
||||
var exchanges = rateFactory.RateProviderFactory
|
||||
.AvailableRateProviders
|
||||
.OrderBy(s => s.Id, StringComparer.OrdinalIgnoreCase)
|
||||
.ToDictionary(k => k.Id, k => k.DisplayName);
|
||||
var recommendation = exchanges.First(e => e.Key == defaultExchange);
|
||||
|
||||
return Ok(new CreateStoreData
|
||||
{
|
||||
DefaultCurrency = defaultCurrency,
|
||||
Exchanges = exchanges,
|
||||
RecommendedExchangeId = recommendation.Key
|
||||
});
|
||||
}
|
||||
}
|
@@ -1,74 +0,0 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayApp.CommonServer;
|
||||
using BTCPayApp.CommonServer.Models;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Fido2;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Authentication.BearerToken;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace BTCPayServer.App.API;
|
||||
|
||||
[ApiController]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.GreenfieldBearer)]
|
||||
[Route("btcpayapp")]
|
||||
public partial class AppApiController(
|
||||
StoreRepository storeRepository,
|
||||
AppService appService,
|
||||
EventAggregator eventAggregator,
|
||||
SignInManager<ApplicationUser> signInManager,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
RoleManager<IdentityRole> roleManager,
|
||||
TimeProvider timeProvider,
|
||||
SettingsRepository settingsRepository,
|
||||
UriResolver uriResolver,
|
||||
DefaultRulesCollection defaultRules,
|
||||
RateFetcher rateFactory,
|
||||
UserLoginCodeService userLoginCodeService,
|
||||
Logs logs,
|
||||
BTCPayServerOptions btcpayOptions,
|
||||
IOptionsMonitor<BearerTokenOptions> bearerTokenOptions)
|
||||
: Controller
|
||||
{
|
||||
private readonly ILogger _logger = logs.PayServer;
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpGet("instance")]
|
||||
public async Task<Results<Ok<AppInstanceInfo>, NotFound>> Instance()
|
||||
{
|
||||
var serverSettings = await settingsRepository.GetSettingAsync<ServerSettings>() ?? new ServerSettings();
|
||||
var policiesSettings = await settingsRepository.GetSettingAsync<PoliciesSettings>() ?? new PoliciesSettings();
|
||||
var themeSettings = await settingsRepository.GetSettingAsync<ThemeSettings>() ?? new ThemeSettings();
|
||||
|
||||
return TypedResults.Ok(new AppInstanceInfo
|
||||
{
|
||||
BaseUrl = Request.GetAbsoluteRoot(),
|
||||
ServerName = serverSettings.ServerName,
|
||||
ContactUrl = serverSettings.ContactUrl,
|
||||
RegistrationEnabled = policiesSettings.EnableRegistration,
|
||||
CustomThemeExtension = themeSettings.CustomTheme ? themeSettings.CustomThemeExtension.ToString() : null,
|
||||
CustomThemeCssUrl = themeSettings.CustomTheme && !string.IsNullOrEmpty(themeSettings.CustomThemeCssUrl?.ToString())
|
||||
? await uriResolver.Resolve(Request.GetAbsoluteRootUri(), themeSettings.CustomThemeCssUrl)
|
||||
: null,
|
||||
LogoUrl = !string.IsNullOrEmpty(themeSettings.LogoUrl?.ToString())
|
||||
? await uriResolver.Resolve(Request.GetAbsoluteRootUri(), themeSettings.LogoUrl)
|
||||
: null
|
||||
});
|
||||
}
|
||||
}
|
@@ -1,41 +0,0 @@
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
|
||||
namespace BTCPayServer.App.API;
|
||||
|
||||
public class ProtobufFormatterAttribute : ActionFilterAttribute, IControllerModelConvention, IActionModelConvention
|
||||
{
|
||||
public void Apply(ControllerModel controller)
|
||||
{
|
||||
foreach (var action in controller.Actions)
|
||||
{
|
||||
Apply(action);
|
||||
}
|
||||
}
|
||||
|
||||
public void Apply(ActionModel action)
|
||||
{
|
||||
// Set the model binder to NewtonsoftJsonBodyModelBinder for parameters that are bound to the request body.
|
||||
var parameters = action.Parameters.Where(p => p.BindingInfo?.BindingSource == BindingSource.Body);
|
||||
foreach (var p in parameters)
|
||||
{
|
||||
p.BindingInfo.BinderType = typeof(ProtobufFormatterModelBinder);
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnActionExecuted(ActionExecutedContext context)
|
||||
{
|
||||
if (context.Result is ObjectResult objectResult)
|
||||
{
|
||||
objectResult.Formatters.Clear();
|
||||
objectResult.Formatters.Add(new ProtobufOutputFormatter());
|
||||
}
|
||||
else
|
||||
{
|
||||
base.OnActionExecuted(context);
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,16 +0,0 @@
|
||||
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace BTCPayServer.App.API;
|
||||
|
||||
public class ProtobufFormatterModelBinder : BodyModelBinder
|
||||
{
|
||||
private static readonly IInputFormatter[] _inputFormatter = [new ProtobufInputFormatter()];
|
||||
|
||||
public ProtobufFormatterModelBinder(ILoggerFactory loggerFactory, IHttpRequestStreamReaderFactory readerFactory) :
|
||||
base(_inputFormatter, readerFactory, loggerFactory)
|
||||
{
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user