Compare commits

...

6 Commits

Author SHA1 Message Date
913ff62f2d Fix migration crash 2024-04-04 22:50:12 +09:00
6cc1751924 The Big Cleanup: Refactor BTCPay internals (#5809) 2024-04-04 16:31:04 +09:00
69b589a401 Adding Tether as BTCPay Server Foundation Supporter (#5891)
* Adding Tether as BTCPay Server Foundation Supporter

* Adding Tether to _BTCPaySupporters partial as well

* Modfying supporter_strike.svg to have white backgroundf or dark mode

* Modifying supporter_tether.svg to fit in the 150x100 box

* Centering Tether shape
2024-04-02 19:06:24 -05:00
db73b1f268 Fix test CheckJSContent 2024-04-01 12:11:00 +09:00
9ac0e982d6 chore: fix typos (#5883) 2024-03-30 10:20:24 +01:00
cb25c225e9 Remove custodians (#5863)
* Remove custodians

* Hide Experimental checkbox in the server policies

---------

Co-authored-by: Nicolas Dorier <nicolas.dorier@gmail.com>
2024-03-29 00:28:27 +09:00
342 changed files with 8399 additions and 13936 deletions

View File

@ -1,5 +1,4 @@
using System;
using System.Data.Common;
using BTCPayServer.Abstractions.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;

View File

@ -1,19 +0,0 @@
namespace BTCPayServer.Abstractions.Custodians.Client;
public class AssetQuoteResult
{
public string FromAsset { get; set; }
public string ToAsset { get; set; }
public decimal Bid { get; set; }
public decimal Ask { get; set; }
public AssetQuoteResult() { }
public AssetQuoteResult(string fromAsset, string toAsset, decimal bid, decimal ask)
{
FromAsset = fromAsset;
ToAsset = toAsset;
Bid = bid;
Ask = ask;
}
}

View File

@ -1,12 +0,0 @@
namespace BTCPayServer.Abstractions.Custodians;
public class AssetBalancesUnavailableException : CustodianApiException
{
public AssetBalancesUnavailableException(System.Exception e) : base(500, "asset-balances-unavailable", $"Cannot fetch the asset balances: {e.Message}", e)
{
}
public AssetBalancesUnavailableException(string errorMsg) : base(500, "asset-balances-unavailable", $"Cannot fetch the asset balances: {errorMsg}")
{
}
}

View File

@ -1,13 +0,0 @@
using BTCPayServer.Client.Models;
namespace BTCPayServer.Abstractions.Custodians;
public class AssetQuoteUnavailableException : CustodianApiException
{
public AssetPairData AssetPair { get; }
public AssetQuoteUnavailableException(AssetPairData assetPair) : base(400, "asset-price-unavailable", "Cannot find a quote for pair " + assetPair)
{
this.AssetPair = assetPair;
}
}

View File

@ -1,13 +0,0 @@
using System;
namespace BTCPayServer.Abstractions.Custodians;
public class BadConfigException : CustodianApiException
{
public string[] BadConfigKeys { get; set; }
public BadConfigException(string[] badConfigKeys) : base(500, "bad-custodian-account-config", "Wrong config values: " + String.Join(", ", badConfigKeys))
{
this.BadConfigKeys = badConfigKeys;
}
}

View File

@ -1,13 +0,0 @@
namespace BTCPayServer.Abstractions.Custodians;
public class CannotWithdrawException : CustodianApiException
{
public CannotWithdrawException(ICustodian custodian, string paymentMethod, string message) : base(403, "cannot-withdraw", message)
{
}
public CannotWithdrawException(ICustodian custodian, string paymentMethod, string targetAddress, CustodianApiException originalException) : base(403, "cannot-withdraw", $"{custodian.Name} cannot withdraw {paymentMethod} to '{targetAddress}': {originalException.Message}")
{
}
}

View File

@ -1,18 +0,0 @@
using System;
namespace BTCPayServer.Abstractions.Custodians;
public class CustodianApiException : Exception
{
public int HttpStatus { get; }
public string Code { get; }
public CustodianApiException(int httpStatus, string code, string message, System.Exception ex) : base(message, ex)
{
HttpStatus = httpStatus;
Code = code;
}
public CustodianApiException(int httpStatus, string code, string message) : this(httpStatus, code, message, null)
{
}
}

View File

@ -1,8 +0,0 @@
namespace BTCPayServer.Abstractions.Custodians;
public class CustodianFeatureNotImplementedException : CustodianApiException
{
public CustodianFeatureNotImplementedException(string message) : base(400, "not-implemented", message)
{
}
}

View File

@ -1,8 +0,0 @@
namespace BTCPayServer.Abstractions.Custodians;
public class DepositsUnavailableException : CustodianApiException
{
public DepositsUnavailableException(string message) : base(404, "deposits-unavailable", message)
{
}
}

View File

@ -1,8 +0,0 @@
namespace BTCPayServer.Abstractions.Custodians;
public class InsufficientFundsException : CustodianApiException
{
public InsufficientFundsException(string message) : base(400, "insufficient-funds", message)
{
}
}

View File

@ -1,9 +0,0 @@
namespace BTCPayServer.Abstractions.Custodians;
public class InvalidWithdrawalTargetException : CustodianApiException
{
public InvalidWithdrawalTargetException(ICustodian custodian, string paymentMethod, string targetAddress, CustodianApiException originalException) : base(403, "invalid-withdrawal-target", $"{custodian.Name} cannot withdraw {paymentMethod} to '{targetAddress}': {originalException.Message}")
{
}
}

View File

@ -1,9 +0,0 @@
namespace BTCPayServer.Abstractions.Custodians;
public class PermissionDeniedCustodianApiException : CustodianApiException
{
public PermissionDeniedCustodianApiException(ICustodian custodian) : base(403, "custodian-api-permission-denied", $"{custodian.Name}'s API reported that you don't have permission.")
{
}
}

View File

@ -1,11 +0,0 @@
namespace BTCPayServer.Abstractions.Custodians;
public class TradeNotFoundException : CustodianApiException
{
private string tradeId { get; }
public TradeNotFoundException(string tradeId) : base(404, "trade-not-found", "Could not find trade ID " + tradeId)
{
this.tradeId = tradeId;
}
}

View File

@ -1,11 +0,0 @@
namespace BTCPayServer.Abstractions.Custodians;
public class WithdrawalNotFoundException : CustodianApiException
{
private string WithdrawalId { get; }
public WithdrawalNotFoundException(string withdrawalId) : base(404, "withdrawal-not-found", $"Could not find withdrawal ID {withdrawalId}.")
{
WithdrawalId = withdrawalId;
}
}

View File

@ -1,9 +0,0 @@
namespace BTCPayServer.Abstractions.Custodians;
public class WrongTradingPairException : CustodianApiException
{
public const int HttpCode = 404;
public WrongTradingPairException(string fromAsset, string toAsset) : base(HttpCode, "wrong-trading-pair", $"Cannot find a trading pair for converting {fromAsset} into {toAsset}.")
{
}
}

View File

@ -1,29 +0,0 @@
using System.Collections.Generic;
using BTCPayServer.Client.Models;
namespace BTCPayServer.Abstractions.Custodians.Client;
/**
* The result of a market trade. Used as a return type for custodians implementing ICanTrade
*/
public class MarketTradeResult
{
public string FromAsset { get; }
public string ToAsset { get; }
/**
* The ledger entries that show the balances that were affected by the trade.
*/
public List<LedgerEntryData> LedgerEntries { get; }
/**
* The unique ID of the trade that was executed.
*/
public string TradeId { get; }
public MarketTradeResult(string fromAsset, string toAsset, List<LedgerEntryData> ledgerEntries, string tradeId)
{
this.FromAsset = fromAsset;
this.ToAsset = toAsset;
this.LedgerEntries = ledgerEntries;
this.TradeId = tradeId;
}
}

View File

@ -1,28 +0,0 @@
using System.Collections.Generic;
using BTCPayServer.Client.Models;
using BTCPayServer.JsonConverters;
namespace BTCPayServer.Abstractions.Custodians.Client;
public class SimulateWithdrawalResult
{
public string PaymentMethod { get; }
public string Asset { get; }
public decimal MinQty { get; }
public decimal MaxQty { get; }
public List<LedgerEntryData> LedgerEntries { get; }
// Fee can be NULL if unknown.
public decimal? Fee { get; }
public SimulateWithdrawalResult(string paymentMethod, string asset, List<LedgerEntryData> ledgerEntries,
decimal minQty, decimal maxQty)
{
PaymentMethod = paymentMethod;
Asset = asset;
LedgerEntries = ledgerEntries;
MinQty = minQty;
MaxQty = maxQty;
}
}

View File

@ -1,29 +0,0 @@
using System;
using System.Collections.Generic;
using BTCPayServer.Client.Models;
namespace BTCPayServer.Abstractions.Custodians.Client;
public class WithdrawResult
{
public string PaymentMethod { get; }
public string Asset { get; set; }
public List<LedgerEntryData> LedgerEntries { get; }
public string WithdrawalId { get; }
public WithdrawalResponseData.WithdrawalStatus Status { get; }
public DateTimeOffset CreatedTime { get; }
public string TargetAddress { get; }
public string TransactionId { get; }
public WithdrawResult(string paymentMethod, string asset, List<LedgerEntryData> ledgerEntries, string withdrawalId, WithdrawalResponseData.WithdrawalStatus status, DateTimeOffset createdTime, string targetAddress, string transactionId)
{
PaymentMethod = paymentMethod;
Asset = asset;
LedgerEntries = ledgerEntries;
WithdrawalId = withdrawalId;
CreatedTime = createdTime;
Status = status;
TargetAddress = targetAddress;
TransactionId = transactionId;
}
}

View File

@ -1,17 +0,0 @@
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Abstractions.Custodians;
public interface ICanDeposit
{
/**
* Get the address where we can deposit for the chosen payment method (crypto code + network).
* The result can be a string in different formats like a bitcoin address or even a LN invoice.
*/
public Task<DepositAddressData> GetDepositAddressAsync(string paymentMethod, JObject config, CancellationToken cancellationToken);
public string[] GetDepositablePaymentMethods();
}

View File

@ -1,31 +0,0 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Custodians.Client;
using BTCPayServer.Client.Models;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Abstractions.Custodians;
public interface ICanTrade
{
/**
* A list of tradable asset pairs, or NULL if the custodian cannot trade/convert assets. if thr asset pair contains fiat, fiat is always put last. If both assets are a cyrptocode or both are fiat, the pair is written alphabetically. Always in uppercase. Example: ["BTC/EUR","BTC/USD", "EUR/USD", "BTC/ETH",...]
*/
public List<AssetPairData> GetTradableAssetPairs();
/**
* Execute a market order right now.
*/
public Task<MarketTradeResult> TradeMarketAsync(string fromAsset, string toAsset, decimal qty, JObject config, CancellationToken cancellationToken);
/**
* Get the details about a previous market trade.
*/
public Task<MarketTradeResult> GetTradeInfoAsync(string tradeId, JObject config, CancellationToken cancellationToken);
public Task<AssetQuoteResult> GetQuoteForAssetAsync(string fromAsset, string toAsset, JObject config, CancellationToken cancellationToken);
}

View File

@ -1,20 +0,0 @@
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Custodians.Client;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Abstractions.Custodians;
/// <summary>
/// Interface for custodians that can move funds to the store wallet.
/// </summary>
public interface ICanWithdraw
{
public Task<WithdrawResult> WithdrawToStoreWalletAsync(string paymentMethod, decimal amount, JObject config, CancellationToken cancellationToken);
public Task<SimulateWithdrawalResult> SimulateWithdrawalAsync(string paymentMethod, decimal qty, JObject config, CancellationToken cancellationToken);
public Task<WithdrawResult> GetWithdrawalInfoAsync(string paymentMethod, string withdrawalId, JObject config, CancellationToken cancellationToken);
public string[] GetWithdrawablePaymentMethods();
}

View File

@ -1,26 +0,0 @@
#nullable enable
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Abstractions.Custodians;
public interface ICustodian
{
/**
* Get the unique code that identifies this custodian.
*/
string Code { get; }
string Name { get; }
/**
* Get a list of assets and their qty in custody.
*/
Task<Dictionary<string, decimal>> GetAssetBalancesAsync(JObject config, CancellationToken cancellationToken);
public Task<Form.Form> GetConfigForm(JObject config, CancellationToken cancellationToken = default);
}

View File

@ -1,14 +0,0 @@
#nullable enable
using System.Collections.Generic;
using System.Linq;
using BTCPayServer.Abstractions.Custodians;
namespace BTCPayServer.Abstractions.Extensions;
public static class CustodianExtensions
{
public static ICustodian? GetCustodianByCode(this IEnumerable<ICustodian> custodians, string code)
{
return custodians.FirstOrDefault(custodian => custodian.Code == code);
}
}

View File

@ -1,102 +0,0 @@
using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
namespace BTCPayServer.Client
{
public partial class BTCPayServerClient
{
public virtual async Task<IEnumerable<CustodianAccountData>> GetCustodianAccounts(string storeId, bool includeAssetBalances = false, CancellationToken token = default)
{
var queryPayload = new Dictionary<string, object>();
if (includeAssetBalances)
{
queryPayload.Add("assetBalances", "true");
}
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/custodian-accounts", queryPayload), token);
return await HandleResponse<IEnumerable<CustodianAccountData>>(response);
}
public virtual async Task<CustodianAccountResponse> GetCustodianAccount(string storeId, string accountId, bool includeAssetBalances = false, CancellationToken token = default)
{
var queryPayload = new Dictionary<string, object>();
if (includeAssetBalances)
{
queryPayload.Add("assetBalances", "true");
}
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/custodian-accounts/{accountId}", queryPayload), token);
return await HandleResponse<CustodianAccountResponse>(response);
}
public virtual async Task<CustodianAccountData> CreateCustodianAccount(string storeId, CreateCustodianAccountRequest request, CancellationToken token = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/custodian-accounts", bodyPayload: request, method: HttpMethod.Post), token);
return await HandleResponse<CustodianAccountData>(response);
}
public virtual async Task<CustodianAccountData> UpdateCustodianAccount(string storeId, string accountId, CreateCustodianAccountRequest request, CancellationToken token = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/custodian-accounts/{accountId}", bodyPayload: request, method: HttpMethod.Put), token);
return await HandleResponse<CustodianAccountData>(response);
}
public virtual async Task DeleteCustodianAccount(string storeId, string accountId, CancellationToken token = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/custodian-accounts/{accountId}", method: HttpMethod.Delete), token);
await HandleResponse(response);
}
public virtual async Task<DepositAddressData> GetCustodianAccountDepositAddress(string storeId, string accountId, string paymentMethod, CancellationToken token = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/custodian-accounts/{accountId}/addresses/{paymentMethod}"), token);
return await HandleResponse<DepositAddressData>(response);
}
public virtual async Task<MarketTradeResponseData> MarketTradeCustodianAccountAsset(string storeId, string accountId, TradeRequestData request, CancellationToken token = default)
{
//var response = await _httpClient.SendAsync(CreateHttpRequest("api/v1/users", null, request, HttpMethod.Post), token);
//return await HandleResponse<ApplicationUserData>(response);
var internalRequest = CreateHttpRequest($"api/v1/stores/{storeId}/custodian-accounts/{accountId}/trades/market", null,
request, HttpMethod.Post);
var response = await _httpClient.SendAsync(internalRequest, token);
return await HandleResponse<MarketTradeResponseData>(response);
}
public virtual async Task<MarketTradeResponseData> GetCustodianAccountTradeInfo(string storeId, string accountId, string tradeId, CancellationToken token = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/custodian-accounts/{accountId}/trades/{tradeId}", method: HttpMethod.Get), token);
return await HandleResponse<MarketTradeResponseData>(response);
}
public virtual async Task<TradeQuoteResponseData> GetCustodianAccountTradeQuote(string storeId, string accountId, string fromAsset, string toAsset, CancellationToken token = default)
{
var queryPayload = new Dictionary<string, object>();
queryPayload.Add("fromAsset", fromAsset);
queryPayload.Add("toAsset", toAsset);
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/custodian-accounts/{accountId}/trades/quote", queryPayload), token);
return await HandleResponse<TradeQuoteResponseData>(response);
}
public virtual async Task<WithdrawalResponseData> CreateCustodianAccountWithdrawal(string storeId, string accountId, WithdrawRequestData request, CancellationToken token = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/custodian-accounts/{accountId}/withdrawals", bodyPayload: request, method: HttpMethod.Post), token);
return await HandleResponse<WithdrawalResponseData>(response);
}
public virtual async Task<WithdrawalSimulationResponseData> SimulateCustodianAccountWithdrawal(string storeId, string accountId, WithdrawRequestData request, CancellationToken token = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/custodian-accounts/{accountId}/withdrawals/simulation", bodyPayload: request, method: HttpMethod.Post), token);
return await HandleResponse<WithdrawalSimulationResponseData>(response);
}
public virtual async Task<WithdrawalResponseData> GetCustodianAccountWithdrawalInfo(string storeId, string accountId, string paymentMethod, string withdrawalId, CancellationToken token = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/custodian-accounts/{accountId}/withdrawals/{paymentMethod}/{withdrawalId}", method: HttpMethod.Get), token);
return await HandleResponse<WithdrawalResponseData>(response);
}
}
}

View File

@ -1,16 +0,0 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
namespace BTCPayServer.Client
{
public partial class BTCPayServerClient
{
public virtual async Task<IEnumerable<CustodianData>> GetCustodians(CancellationToken token = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest("api/v1/custodians"), token);
return await HandleResponse<IEnumerable<CustodianData>>(response);
}
}
}

View File

@ -1,59 +0,0 @@
using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
namespace BTCPayServer.Client
{
public partial class BTCPayServerClient
{
public virtual async Task<IEnumerable<LNURLPayPaymentMethodData>>
GetStoreLNURLPayPaymentMethods(string storeId, bool? enabled = null,
CancellationToken token = default)
{
var query = new Dictionary<string, object>();
if (enabled != null)
{
query.Add(nameof(enabled), enabled);
}
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/LNURLPay",
query), token);
return await HandleResponse<IEnumerable<LNURLPayPaymentMethodData>>(response);
}
public virtual async Task<LNURLPayPaymentMethodData> GetStoreLNURLPayPaymentMethod(
string storeId,
string cryptoCode, CancellationToken token = default)
{
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/LNURLPay/{cryptoCode}"), token);
return await HandleResponse<LNURLPayPaymentMethodData>(response);
}
public virtual async Task RemoveStoreLNURLPayPaymentMethod(string storeId,
string cryptoCode, CancellationToken token = default)
{
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/LNURLPay/{cryptoCode}",
method: HttpMethod.Delete), token);
await HandleResponse(response);
}
public virtual async Task<LNURLPayPaymentMethodData> UpdateStoreLNURLPayPaymentMethod(
string storeId,
string cryptoCode, LNURLPayPaymentMethodData paymentMethod,
CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/LNURLPay/{cryptoCode}",
bodyPayload: paymentMethod, method: HttpMethod.Put), token);
return await HandleResponse<LNURLPayPaymentMethodData>(response);
}
}
}

View File

@ -1,59 +0,0 @@
using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
namespace BTCPayServer.Client
{
public partial class BTCPayServerClient
{
public virtual async Task<IEnumerable<LightningNetworkPaymentMethodData>>
GetStoreLightningNetworkPaymentMethods(string storeId, bool? enabled = null,
CancellationToken token = default)
{
var query = new Dictionary<string, object>();
if (enabled != null)
{
query.Add(nameof(enabled), enabled);
}
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/LightningNetwork",
query), token);
return await HandleResponse<IEnumerable<LightningNetworkPaymentMethodData>>(response);
}
public virtual async Task<LightningNetworkPaymentMethodData> GetStoreLightningNetworkPaymentMethod(
string storeId,
string cryptoCode, CancellationToken token = default)
{
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/LightningNetwork/{cryptoCode}"), token);
return await HandleResponse<LightningNetworkPaymentMethodData>(response);
}
public virtual async Task RemoveStoreLightningNetworkPaymentMethod(string storeId,
string cryptoCode, CancellationToken token = default)
{
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/LightningNetwork/{cryptoCode}",
method: HttpMethod.Delete), token);
await HandleResponse(response);
}
public virtual async Task<LightningNetworkPaymentMethodData> UpdateStoreLightningNetworkPaymentMethod(
string storeId,
string cryptoCode, UpdateLightningNetworkPaymentMethodRequest paymentMethod,
CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/LightningNetwork/{cryptoCode}",
bodyPayload: paymentMethod, method: HttpMethod.Put), token);
return await HandleResponse<LightningNetworkPaymentMethodData>(response);
}
}
}

View File

@ -3,92 +3,47 @@ using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Client
{
public partial class BTCPayServerClient
{
public virtual async Task<IEnumerable<OnChainPaymentMethodData>> GetStoreOnChainPaymentMethods(string storeId,
bool? enabled = null,
CancellationToken token = default)
{
var query = new Dictionary<string, object>();
if (enabled != null)
{
query.Add(nameof(enabled), enabled);
}
public partial class BTCPayServerClient
{
public virtual async Task<OnChainPaymentMethodPreviewResultData>
PreviewProposedStoreOnChainPaymentMethodAddresses(
string storeId, string paymentMethodId, string derivationScheme, int offset = 0,
int amount = 10,
CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/{paymentMethodId}/preview",
bodyPayload: new UpdatePaymentMethodRequest() { Config = JValue.CreateString(derivationScheme) },
queryPayload: new Dictionary<string, object>() { { "offset", offset }, { "amount", amount } },
method: HttpMethod.Post), token);
return await HandleResponse<OnChainPaymentMethodPreviewResultData>(response);
}
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain",
query), token);
return await HandleResponse<IEnumerable<OnChainPaymentMethodData>>(response);
}
public virtual async Task<OnChainPaymentMethodPreviewResultData> PreviewStoreOnChainPaymentMethodAddresses(
string storeId, string paymentMethodId, int offset = 0, int amount = 10,
CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/{paymentMethodId}/preview",
queryPayload: new Dictionary<string, object>() { { "offset", offset }, { "amount", amount } },
method: HttpMethod.Get), token);
return await HandleResponse<OnChainPaymentMethodPreviewResultData>(response);
}
public virtual async Task<OnChainPaymentMethodData> GetStoreOnChainPaymentMethod(string storeId,
string cryptoCode, CancellationToken token = default)
{
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}"), token);
return await HandleResponse<OnChainPaymentMethodData>(response);
}
public virtual async Task<GenerateOnChainWalletResponse> GenerateOnChainWallet(string storeId,
string paymentMethodId, GenerateOnChainWalletRequest request,
CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/{paymentMethodId}/generate",
bodyPayload: request,
method: HttpMethod.Post), token);
return await HandleResponse<GenerateOnChainWalletResponse>(response);
}
public virtual async Task RemoveStoreOnChainPaymentMethod(string storeId,
string cryptoCode, CancellationToken token = default)
{
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}",
method: HttpMethod.Delete), token);
await HandleResponse(response);
}
public virtual async Task<OnChainPaymentMethodData> UpdateStoreOnChainPaymentMethod(string storeId,
string cryptoCode, UpdateOnChainPaymentMethodRequest paymentMethod,
CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}",
bodyPayload: paymentMethod, method: HttpMethod.Put), token);
return await HandleResponse<OnChainPaymentMethodData>(response);
}
public virtual async Task<OnChainPaymentMethodPreviewResultData>
PreviewProposedStoreOnChainPaymentMethodAddresses(
string storeId, string cryptoCode, UpdateOnChainPaymentMethodRequest paymentMethod, int offset = 0,
int amount = 10,
CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/preview",
bodyPayload: paymentMethod,
queryPayload: new Dictionary<string, object>() { { "offset", offset }, { "amount", amount } },
method: HttpMethod.Post), token);
return await HandleResponse<OnChainPaymentMethodPreviewResultData>(response);
}
public virtual async Task<OnChainPaymentMethodPreviewResultData> PreviewStoreOnChainPaymentMethodAddresses(
string storeId, string cryptoCode, int offset = 0, int amount = 10,
CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/preview",
queryPayload: new Dictionary<string, object>() { { "offset", offset }, { "amount", amount } },
method: HttpMethod.Get), token);
return await HandleResponse<OnChainPaymentMethodPreviewResultData>(response);
}
public virtual async Task<OnChainPaymentMethodDataWithSensitiveData> GenerateOnChainWallet(string storeId,
string cryptoCode, GenerateOnChainWalletRequest request,
CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/generate",
bodyPayload: request,
method: HttpMethod.Post), token);
return await HandleResponse<OnChainPaymentMethodDataWithSensitiveData>(response);
}
}
}
}

View File

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
@ -7,21 +8,60 @@ namespace BTCPayServer.Client
{
public partial class BTCPayServerClient
{
public virtual async Task<Dictionary<string, GenericPaymentMethodData>> GetStorePaymentMethods(string storeId,
bool? enabled = null,
public virtual async Task<GenericPaymentMethodData> UpdateStorePaymentMethod(
string storeId,
string paymentMethodId,
UpdatePaymentMethodRequest request,
CancellationToken token = default)
{
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/{paymentMethodId}", bodyPayload: request, method: HttpMethod.Put),
token);
return await HandleResponse<GenericPaymentMethodData>(response);
}
public virtual async Task RemoveStorePaymentMethod(string storeId, string paymentMethodId)
{
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/{paymentMethodId}", method: HttpMethod.Delete),
CancellationToken.None);
await HandleResponse(response);
}
public virtual async Task<GenericPaymentMethodData> GetStorePaymentMethod(string storeId,
string paymentMethodId, bool? includeConfig = null, CancellationToken token = default)
{
var query = new Dictionary<string, object>();
if (enabled != null)
if (includeConfig != null)
{
query.Add(nameof(enabled), enabled);
query.Add(nameof(includeConfig), includeConfig);
}
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/{paymentMethodId}",
query), token);
return await HandleResponse<GenericPaymentMethodData>(response);
}
public virtual async Task<GenericPaymentMethodData[]> GetStorePaymentMethods(string storeId,
bool? onlyEnabled = null, bool? includeConfig = null, CancellationToken token = default)
{
var query = new Dictionary<string, object>();
if (onlyEnabled != null)
{
query.Add(nameof(onlyEnabled), onlyEnabled);
}
if (includeConfig != null)
{
query.Add(nameof(includeConfig), includeConfig);
}
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods",
query), token);
return await HandleResponse<Dictionary<string, GenericPaymentMethodData>>(response);
return await HandleResponse<GenericPaymentMethodData[]>(response);
}
}
}

View File

@ -0,0 +1,43 @@
using NBitcoin;
using NBitcoin.DataEncoders;
using NBitcoin.JsonConverters;
using Newtonsoft.Json;
using System;
using System.IO;
using System.Reflection;
namespace BTCPayServer.Client.JsonConverters
{
public class SaneOutpointJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return typeof(OutPoint).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo());
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
return null;
if (reader.TokenType != JsonToken.String)
throw new JsonObjectException($"Unexpected json token type, expected is {JsonToken.String} and actual is {reader.TokenType}", reader);
try
{
if (!OutPoint.TryParse((string)reader.Value, out var outpoint))
throw new JsonObjectException("Invalid bitcoin object of type OutPoint", reader);
return outpoint;
}
catch (EndOfStreamException)
{
}
throw new JsonObjectException("Invalid bitcoin object of type OutPoint", reader);
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if (value is { })
writer.WriteValue(value.ToString());
}
}
}

View File

@ -1,36 +0,0 @@
using System;
using System.Globalization;
using BTCPayServer.Client.Models;
using BTCPayServer.Lightning;
using NBitcoin.JsonConverters;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Client.JsonConverters
{
public class TradeQuantityJsonConverter : JsonConverter<TradeQuantity>
{
public override TradeQuantity ReadJson(JsonReader reader, Type objectType, TradeQuantity existingValue, bool hasExistingValue, JsonSerializer serializer)
{
JToken token = JToken.Load(reader);
switch (token.Type)
{
case JTokenType.Float:
case JTokenType.Integer:
case JTokenType.String:
if (TradeQuantity.TryParse(token.ToString(), out var q))
return q;
break;
case JTokenType.Null:
return null;
}
throw new JsonObjectException("Invalid TradeQuantity, expected string. Expected: \"1.50\" or \"50%\"", reader);
}
public override void WriteJson(JsonWriter writer, TradeQuantity value, JsonSerializer serializer)
{
if (value is not null)
writer.WriteValue(value.ToString());
}
}
}

View File

@ -1,12 +0,0 @@
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Client.Models
{
public class CreateCustodianAccountRequest
{
public string CustodianCode { get; set; }
public string Name { get; set; }
public JObject Config { get; set; }
}
}

View File

@ -1,4 +1,5 @@
using System.Collections.Generic;
using BTCPayServer.Client.JsonConverters;
using BTCPayServer.JsonConverters;
using NBitcoin;
using NBitcoin.JsonConverters;
@ -21,7 +22,7 @@ namespace BTCPayServer.Client.Models
public bool ProceedWithPayjoin { get; set; } = true;
public bool ProceedWithBroadcast { get; set; } = true;
public bool NoChange { get; set; } = false;
[JsonProperty(ItemConverterType = typeof(OutpointJsonConverter))]
[JsonProperty(ItemConverterType = typeof(SaneOutpointJsonConverter))]
public List<OutPoint> SelectedInputs { get; set; } = null;
public List<CreateOnChainTransactionRequestDestination> Destinations { get; set; }
[JsonProperty("rbf")]

View File

@ -1,16 +0,0 @@
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Client.Models
{
public abstract class CustodianAccountBaseData
{
public string CustodianCode { get; set; }
public string Name { get; set; }
public string StoreId { get; set; }
public JObject Config { get; set; }
}
}

View File

@ -1,7 +0,0 @@
namespace BTCPayServer.Client.Models
{
public class CustodianAccountData : CustodianAccountBaseData
{
public string Id { get; set; }
}
}

View File

@ -1,14 +0,0 @@
using System.Collections.Generic;
namespace BTCPayServer.Client.Models;
public class CustodianAccountResponse : CustodianAccountData
{
public IDictionary<string, decimal> AssetBalances { get; set; }
public CustodianAccountResponse()
{
}
}

View File

@ -1,13 +0,0 @@
using System.Collections.Generic;
namespace BTCPayServer.Client.Models;
public class CustodianData
{
public string Code { get; set; }
public string Name { get; set; }
public Dictionary<string, AssetPairData> TradableAssetPairs { get; set; }
public string[] WithdrawablePaymentMethods { get; set; }
public string[] DepositablePaymentMethods { get; set; }
}

View File

@ -1,15 +0,0 @@
namespace BTCPayServer.Client.Models;
public class DepositAddressData
{
// /**
// * Example: P2PKH, P2SH, P2WPKH, P2TR, BOLT11, ...
// */
// public string Type { get; set; }
/**
* Format depends hugely on the type.
*/
public string Address { get; set; }
}

View File

@ -1,7 +1,10 @@
using System.Collections.Generic;
using BTCPayServer.Client.JsonConverters;
using BTCPayServer.Client.Models;
using NBitcoin;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Client
{
@ -22,4 +25,16 @@ namespace BTCPayServer.Client
public bool ImportKeysToRPC { get; set; }
public bool SavePrivateKeys { get; set; }
}
public class GenerateOnChainWalletResponse : GenericPaymentMethodData
{
public class ConfigData
{
public string AccountDerivation { get; set; }
[JsonExtensionData]
IDictionary<string, JToken> AdditionalData { get; set; }
}
[JsonConverter(typeof(MnemonicJsonConverter))]
public Mnemonic Mnemonic { get; set; }
public new ConfigData Config { get; set; }
}
}

View File

@ -1,9 +1,22 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Client.Models
{
public class GenericPaymentMethodData
{
public bool Enabled { get; set; }
public object Data { get; set; }
public string CryptoCode { get; set; }
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public JToken Config { get; set; }
public string PaymentMethodId { get; set; }
}
public class UpdatePaymentMethodRequest
{
public UpdatePaymentMethodRequest()
{
}
public bool? Enabled { get; set; }
public JToken Config { get; set; }
}
}

View File

@ -29,13 +29,12 @@ namespace BTCPayServer.Client.Models
public decimal Amount { get; set; }
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal NetworkFee { get; set; }
public decimal PaymentMethodFee { get; set; }
public List<Payment> Payments { get; set; }
public string PaymentMethod { get; set; }
public string CryptoCode { get; set; }
public JObject AdditionalData { get; set; }
public string PaymentMethodId { get; set; }
public JToken AdditionalData { get; set; }
public string Currency { get; set; }
public class Payment
{

View File

@ -1,17 +0,0 @@
using Newtonsoft.Json;
namespace BTCPayServer.Client.Models
{
public class LNURLPayPaymentMethodBaseData
{
public bool UseBech32Scheme { get; set; }
[JsonProperty("lud12Enabled")]
public bool LUD12Enabled { get; set; }
public LNURLPayPaymentMethodBaseData()
{
}
}
}

View File

@ -1,27 +0,0 @@
namespace BTCPayServer.Client.Models
{
public class LNURLPayPaymentMethodData : LNURLPayPaymentMethodBaseData
{
/// <summary>
/// Whether the payment method is enabled
/// </summary>
public bool Enabled { get; set; }
/// <summary>
/// Crypto code of the payment method
/// </summary>
public string CryptoCode { get; set; }
public LNURLPayPaymentMethodData()
{
}
public LNURLPayPaymentMethodData(string cryptoCode, bool enabled, bool useBech32Scheme, bool lud12Enabled)
{
Enabled = enabled;
CryptoCode = cryptoCode;
UseBech32Scheme = useBech32Scheme;
LUD12Enabled = lud12Enabled;
}
}
}

View File

@ -1,12 +0,0 @@
namespace BTCPayServer.Client.Models
{
public class LightningNetworkPaymentMethodBaseData
{
public string ConnectionString { get; set; }
public LightningNetworkPaymentMethodBaseData()
{
}
}
}

View File

@ -1,29 +0,0 @@
namespace BTCPayServer.Client.Models
{
public class LightningNetworkPaymentMethodData : LightningNetworkPaymentMethodBaseData
{
/// <summary>
/// Whether the payment method is enabled
/// </summary>
public bool Enabled { get; set; }
/// <summary>
/// Crypto code of the payment method
/// </summary>
public string CryptoCode { get; set; }
public LightningNetworkPaymentMethodData()
{
}
public LightningNetworkPaymentMethodData(string cryptoCode, string connectionString, bool enabled, string paymentMethod)
{
Enabled = enabled;
CryptoCode = cryptoCode;
ConnectionString = connectionString;
PaymentMethod = paymentMethod;
}
public string PaymentMethod { get; set; }
}
}

View File

@ -1,31 +0,0 @@
using System.Collections.Generic;
namespace BTCPayServer.Client.Models;
public class MarketTradeResponseData
{
public string FromAsset { get; }
public string ToAsset { get; }
/**
* The ledger entries that show the balances that were affected by the trade.
*/
public List<LedgerEntryData> LedgerEntries { get; }
/**
* The unique ID of the trade that was executed.
*/
public string TradeId { get; }
public string AccountId { get; }
public string CustodianCode { get; }
public MarketTradeResponseData(string fromAsset, string toAsset, List<LedgerEntryData> ledgerEntries, string tradeId, string accountId, string custodianCode)
{
FromAsset = fromAsset;
ToAsset = toAsset;
LedgerEntries = ledgerEntries;
TradeId = tradeId;
AccountId = accountId;
CustodianCode = custodianCode;
}
}

View File

@ -1,24 +0,0 @@
using NBitcoin;
using Newtonsoft.Json;
namespace BTCPayServer.Client.Models
{
public class OnChainPaymentMethodBaseData
{
/// <summary>
/// The derivation scheme
/// </summary>
public string DerivationScheme { get; set; }
public string Label { get; set; }
[JsonConverter(typeof(NBitcoin.JsonConverters.KeyPathJsonConverter))]
public RootedKeyPath AccountKeyPath { get; set; }
public OnChainPaymentMethodBaseData()
{
}
}
}

View File

@ -1,47 +0,0 @@
using NBitcoin;
namespace BTCPayServer.Client.Models
{
public class OnChainPaymentMethodDataPreview : OnChainPaymentMethodBaseData
{
/// <summary>
/// Crypto code of the payment method
/// </summary>
public string CryptoCode { get; set; }
public OnChainPaymentMethodDataPreview()
{
}
public OnChainPaymentMethodDataPreview(string cryptoCode, string derivationScheme, string label, RootedKeyPath accountKeyPath)
{
Label = label;
AccountKeyPath = accountKeyPath;
CryptoCode = cryptoCode;
DerivationScheme = derivationScheme;
}
}
public class OnChainPaymentMethodData : OnChainPaymentMethodDataPreview
{
/// <summary>
/// Whether the payment method is enabled
/// </summary>
public bool Enabled { get; set; }
public string PaymentMethod { get; set; }
public OnChainPaymentMethodData()
{
}
public OnChainPaymentMethodData(string cryptoCode, string derivationScheme, bool enabled, string label, RootedKeyPath accountKeyPath, string paymentMethod) :
base(cryptoCode, derivationScheme, label, accountKeyPath)
{
Enabled = enabled;
PaymentMethod = paymentMethod;
}
}
}

View File

@ -1,23 +0,0 @@
using BTCPayServer.Client.JsonConverters;
using NBitcoin;
using Newtonsoft.Json;
namespace BTCPayServer.Client.Models
{
public class OnChainPaymentMethodDataWithSensitiveData : OnChainPaymentMethodData
{
public OnChainPaymentMethodDataWithSensitiveData()
{
}
public OnChainPaymentMethodDataWithSensitiveData(string cryptoCode, string derivationScheme, bool enabled,
string label, RootedKeyPath accountKeyPath, Mnemonic mnemonic, string paymentMethod) : base(cryptoCode, derivationScheme, enabled,
label, accountKeyPath, paymentMethod)
{
Mnemonic = mnemonic;
}
[JsonConverter(typeof(MnemonicJsonConverter))]
public Mnemonic Mnemonic { get; set; }
}
}

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using BTCPayServer.Client.JsonConverters;
using BTCPayServer.JsonConverters;
using NBitcoin;
using NBitcoin.JsonConverters;
@ -12,7 +13,7 @@ namespace BTCPayServer.Client.Models
public string Comment { get; set; }
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal Amount { get; set; }
[JsonConverter(typeof(OutpointJsonConverter))]
[JsonConverter(typeof(SaneOutpointJsonConverter))]
public OutPoint Outpoint { get; set; }
public string Link { get; set; }
#pragma warning disable CS0612 // Type or member is obsolete

View File

@ -1,22 +0,0 @@
using BTCPayServer.JsonConverters;
using Newtonsoft.Json;
namespace BTCPayServer.Client.Models;
public class TradeQuoteResponseData
{
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal Bid { get; }
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal Ask { get; }
public string ToAsset { get; }
public string FromAsset { get; }
public TradeQuoteResponseData(string fromAsset, string toAsset, decimal bid, decimal ask)
{
FromAsset = fromAsset;
ToAsset = toAsset;
Bid = bid;
Ask = ask;
}
}

View File

@ -1,11 +0,0 @@
using Newtonsoft.Json;
namespace BTCPayServer.Client.Models;
public class TradeRequestData
{
public string FromAsset { set; get; }
public string ToAsset { set; get; }
[JsonConverter(typeof(JsonConverters.TradeQuantityJsonConverter))]
public TradeQuantity Qty { set; get; }
}

View File

@ -1,20 +0,0 @@
namespace BTCPayServer.Client.Models
{
public class UpdateLightningNetworkPaymentMethodRequest : LightningNetworkPaymentMethodBaseData
{
/// <summary>
/// Whether the payment method is enabled
/// </summary>
public bool Enabled { get; set; }
public UpdateLightningNetworkPaymentMethodRequest()
{
}
public UpdateLightningNetworkPaymentMethodRequest(string connectionString, bool enabled)
{
Enabled = enabled;
ConnectionString = connectionString;
}
}
}

View File

@ -1,25 +0,0 @@
using NBitcoin;
namespace BTCPayServer.Client.Models
{
public class UpdateOnChainPaymentMethodRequest : OnChainPaymentMethodBaseData
{
/// <summary>
/// Whether the payment method is enabled
/// </summary>
public bool Enabled { get; set; }
public UpdateOnChainPaymentMethodRequest()
{
}
public UpdateOnChainPaymentMethodRequest(bool enabled, string derivationScheme, string label, RootedKeyPath accountKeyPath)
{
Enabled = enabled;
Label = label;
AccountKeyPath = accountKeyPath;
DerivationScheme = derivationScheme;
}
}
}

View File

@ -91,7 +91,7 @@ namespace BTCPayServer.Client.Models
}
public bool AfterExpiration { get; set; }
public string PaymentMethod { get; set; }
public string PaymentMethodId { get; set; }
public InvoicePaymentMethodDataModel.Payment Payment { get; set; }
}

View File

@ -1,85 +0,0 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Net.Http.Headers;
using Newtonsoft.Json;
namespace BTCPayServer.Client.Models;
public class WithdrawRequestData
{
public string PaymentMethod { set; get; }
[JsonConverter(typeof(JsonConverters.TradeQuantityJsonConverter))]
public TradeQuantity Qty { set; get; }
public WithdrawRequestData()
{
}
public WithdrawRequestData(string paymentMethod, TradeQuantity qty)
{
PaymentMethod = paymentMethod;
Qty = qty;
}
}
#nullable enable
public record TradeQuantity
{
public TradeQuantity(decimal value, ValueType type)
{
Type = type;
Value = value;
}
public enum ValueType
{
Exact,
Percent
}
public ValueType Type { get; }
public decimal Value { get; set; }
public override string ToString()
{
if (Type == ValueType.Exact)
return Value.ToString(CultureInfo.InvariantCulture);
else
return Value.ToString(CultureInfo.InvariantCulture) + "%";
}
public static TradeQuantity Parse(string str)
{
if (!TryParse(str, out var r))
throw new FormatException("Invalid TradeQuantity");
return r;
}
public static bool TryParse(string str, [MaybeNullWhen(false)] out TradeQuantity quantity)
{
if (str is null)
throw new ArgumentNullException(nameof(str));
quantity = null;
str = str.Trim();
str = str.Replace(" ", "");
if (str.Length == 0)
return false;
if (str[^1] == '%')
{
if (!decimal.TryParse(str[..^1], NumberStyles.Any, CultureInfo.InvariantCulture, out var r))
return false;
if (r < 0.0m)
return false;
quantity = new TradeQuantity(r, TradeQuantity.ValueType.Percent);
}
else
{
if (!decimal.TryParse(str, NumberStyles.Any, CultureInfo.InvariantCulture, out var r))
return false;
if (r < 0.0m)
return false;
quantity = new TradeQuantity(r, TradeQuantity.ValueType.Exact);
}
return true;
}
}

View File

@ -1,22 +0,0 @@
using System.Collections.Generic;
namespace BTCPayServer.Client.Models;
public abstract class WithdrawalBaseResponseData
{
public string Asset { get; }
public string PaymentMethod { get; }
public List<LedgerEntryData> LedgerEntries { get; }
public string AccountId { get; }
public string CustodianCode { get; }
public WithdrawalBaseResponseData(string paymentMethod, string asset, List<LedgerEntryData> ledgerEntries, string accountId,
string custodianCode)
{
PaymentMethod = paymentMethod;
Asset = asset;
LedgerEntries = ledgerEntries;
AccountId = accountId;
CustodianCode = custodianCode;
}
}

View File

@ -1,40 +0,0 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
namespace BTCPayServer.Client.Models;
public class WithdrawalResponseData : WithdrawalBaseResponseData
{
[JsonConverter(typeof(StringEnumConverter))]
public WithdrawalStatus Status { get; }
public string WithdrawalId { get; }
public DateTimeOffset CreatedTime { get; }
public string TransactionId { get; }
public string TargetAddress { get; }
public WithdrawalResponseData(string paymentMethod, string asset, List<LedgerEntryData> ledgerEntries, string withdrawalId, string accountId,
string custodianCode, WithdrawalStatus status, DateTimeOffset createdTime, string targetAddress, string transactionId) : base(paymentMethod, asset, ledgerEntries, accountId,
custodianCode)
{
WithdrawalId = withdrawalId;
TargetAddress = targetAddress;
TransactionId = transactionId;
Status = status;
CreatedTime = createdTime;
}
public enum WithdrawalStatus
{
Unknown = 0,
Queued = 1,
Complete = 2,
Failed = 3
}
}

View File

@ -1,21 +0,0 @@
using System.Collections.Generic;
using BTCPayServer.JsonConverters;
using Newtonsoft.Json;
namespace BTCPayServer.Client.Models;
public class WithdrawalSimulationResponseData : WithdrawalBaseResponseData
{
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal? MinQty { get; set; }
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal? MaxQty { get; set; }
public WithdrawalSimulationResponseData(string paymentMethod, string asset, string accountId,
string custodianCode, List<LedgerEntryData> ledgerEntries, decimal? minQty, decimal? maxQty) : base(paymentMethod,
asset, ledgerEntries, accountId, custodianCode)
{
MinQty = minQty;
MaxQty = maxQty;
}
}

View File

@ -40,11 +40,6 @@ namespace BTCPayServer.Client
public const string CanCreatePullPayments = "btcpay.store.cancreatepullpayments";
public const string CanViewPullPayments = "btcpay.store.canviewpullpayments";
public const string CanCreateNonApprovedPullPayments = "btcpay.store.cancreatenonapprovedpullpayments";
public const string CanViewCustodianAccounts = "btcpay.store.canviewcustodianaccounts";
public const string CanManageCustodianAccounts = "btcpay.store.canmanagecustodianaccounts";
public const string CanDepositToCustodianAccounts = "btcpay.store.candeposittocustodianaccount";
public const string CanWithdrawFromCustodianAccounts = "btcpay.store.canwithdrawfromcustodianaccount";
public const string CanTradeCustodianAccount = "btcpay.store.cantradecustodianaccount";
public const string Unrestricted = "unrestricted";
public static IEnumerable<string> AllPolicies
{
@ -79,11 +74,6 @@ namespace BTCPayServer.Client
yield return CanCreatePullPayments;
yield return CanViewPullPayments;
yield return CanCreateNonApprovedPullPayments;
yield return CanViewCustodianAccounts;
yield return CanManageCustodianAccounts;
yield return CanDepositToCustodianAccounts;
yield return CanWithdrawFromCustodianAccounts;
yield return CanTradeCustodianAccount;
yield return CanManageUsers;
yield return CanManagePayouts;
yield return CanViewPayouts;
@ -254,7 +244,6 @@ namespace BTCPayServer.Client
{
var policyMap = new Dictionary<string, HashSet<string>>();
PolicyHasChild(policyMap, Policies.CanModifyStoreSettings,
Policies.CanManageCustodianAccounts,
Policies.CanManagePullPayments,
Policies.CanModifyInvoices,
Policies.CanViewStoreSettings,
@ -275,7 +264,6 @@ namespace BTCPayServer.Client
Policies.CanUseInternalLightningNode,
Policies.CanManageUsers);
PolicyHasChild(policyMap, Policies.CanUseInternalLightningNode, Policies.CanCreateLightningInvoiceInternalNode, Policies.CanViewLightningInvoiceInternalNode);
PolicyHasChild(policyMap, Policies.CanManageCustodianAccounts, Policies.CanViewCustodianAccounts);
PolicyHasChild(policyMap, Policies.CanModifyInvoices, Policies.CanViewInvoices, Policies.CanCreateInvoice, Policies.CanCreateLightningInvoiceInStore);
PolicyHasChild(policyMap, Policies.CanViewStoreSettings, Policies.CanViewInvoices, Policies.CanViewPaymentRequests, Policies.CanViewReports, Policies.CanViewPullPayments, Policies.CanViewPayouts);
PolicyHasChild(policyMap, Policies.CanManagePayouts, Policies.CanViewPayouts);

View File

@ -130,6 +130,11 @@ namespace BTCPayServer
{
return transactionInformationSet;
}
public string GetTrackedDestination(Script scriptPubKey)
{
return scriptPubKey.Hash.ToString() + "#" + CryptoCode.ToUpperInvariant();
}
}
public abstract class BTCPayNetworkBase

View File

@ -39,7 +39,6 @@ namespace BTCPayServer.Data
public DbSet<AddressInvoiceData> AddressInvoices { get; set; }
public DbSet<APIKeyData> ApiKeys { get; set; }
public DbSet<AppData> Apps { get; set; }
public DbSet<CustodianAccountData> CustodianAccount { get; set; }
public DbSet<StoredFile> Files { get; set; }
public DbSet<InvoiceEventData> InvoiceEvents { get; set; }
public DbSet<InvoiceSearchData> InvoiceSearches { get; set; }
@ -94,7 +93,6 @@ namespace BTCPayServer.Data
AddressInvoiceData.OnModelCreating(builder);
APIKeyData.OnModelCreating(builder, Database);
AppData.OnModelCreating(builder, Database);
CustodianAccountData.OnModelCreating(builder, Database);
//StoredFile.OnModelCreating(builder);
InvoiceEventData.OnModelCreating(builder);
InvoiceSearchData.OnModelCreating(builder);

View File

@ -17,6 +17,7 @@ namespace BTCPayServer.Data
public override ApplicationDbContext CreateContext()
{
var builder = new DbContextOptionsBuilder<ApplicationDbContext>();
builder.AddInterceptors(Data.InvoiceData.MigrationInterceptor.Instance);
ConfigureBuilder(builder);
return new ApplicationDbContext(builder.Options);
}

View File

@ -8,6 +8,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.1" />
<PackageReference Include="NBitcoin.Altcoins" Version="3.0.23" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BTCPayServer.Abstractions\BTCPayServer.Abstractions.csproj" />

View File

@ -6,11 +6,6 @@ namespace BTCPayServer.Data
{
public class AddressInvoiceData
{
/// <summary>
/// Some crypto currencies share same address prefix
/// For not having exceptions thrown by two address on different network, we suffix by "#CRYPTOCODE"
/// </summary>
[Obsolete("Use GetHash instead")]
public string Address { get; set; }
public InvoiceData InvoiceData { get; set; }
public string InvoiceDataId { get; set; }

View File

@ -1,54 +0,0 @@
using System;
using System.ComponentModel.DataAnnotations;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Data;
public class CustodianAccountData : IHasBlob<JObject>
{
[Required]
[MaxLength(50)]
public string Id { get; set; }
[Required]
[MaxLength(50)]
public string StoreId { get; set; }
[Required]
[MaxLength(50)]
public string CustodianCode { get; set; }
[Required]
[MaxLength(50)]
public string Name { get; set; }
[JsonIgnore]
[Obsolete("Use Blob2 instead")]
public byte[] Blob { get; set; }
[JsonIgnore]
public string Blob2 { get; set; }
[JsonIgnore]
public StoreData StoreData { get; set; }
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
{
builder.Entity<CustodianAccountData>()
.HasOne(o => o.StoreData)
.WithMany(i => i.CustodianAccounts)
.HasForeignKey(i => i.StoreId).OnDelete(DeleteBehavior.Cascade);
builder.Entity<CustodianAccountData>()
.HasIndex(o => o.StoreId);
if (databaseFacade.IsNpgsql())
{
builder.Entity<CustodianAccountData>()
.Property(o => o.Blob2)
.HasColumnType("JSONB");
}
}
}

View File

@ -0,0 +1,370 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.IO.Compression;
using System.IO;
using System.Text;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Newtonsoft.Json.Linq;
using System.Linq;
using System.Globalization;
using Newtonsoft.Json;
using Microsoft.EntityFrameworkCore.Diagnostics;
using BTCPayServer.Migrations;
using Newtonsoft.Json.Serialization;
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
namespace BTCPayServer.Data
{
public partial class InvoiceData
{
/// <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();
}
return entity;
}
}
static HashSet<string> superflousProperties = new HashSet<string>()
{
"availableAddressHashes",
"events",
"refunds",
"paidAmount",
"historicalAddresses",
"refundable",
"status",
"exceptionStatus",
"storeId",
"id",
"txFee",
"refundMail",
"rate",
"depositAddress",
"currency",
"price",
"payments",
"orderId",
"buyerInformation",
"productInformation",
"derivationStrategy",
"archived",
"isUnderPaid",
"requiresRefundEmail",
"invoiceTime"
};
#pragma warning disable CS0618 // Type or member is obsolete
public void Migrate()
{
if (Currency is not null)
return;
if (Blob is not (null or { Length: 0 }))
{
Blob2 = MigrationExtensions.Unzip(Blob);
Blob = null;
}
var blob = JObject.Parse(Blob2);
if (blob["cryptoData"]?["BTC"] is not (null or { Type: JTokenType.Null }))
{
blob.Move(["rate"], ["cryptoData", "BTC", "rate"]);
blob.Move(["txFee"], ["cryptoData", "BTC", "txFee"]);
}
blob.Move(["customerEmail"], ["metadata", "buyerEmail"]);
foreach (var prop in (blob["cryptoData"] as JObject)?.Properties()?.ToList() ?? [])
{
// We should only change data for onchain
if (prop.Name.Contains('_', StringComparison.OrdinalIgnoreCase))
{
if (prop.Value is JObject pm)
{
pm.Remove("depositAddress");
pm.Remove("feeRate");
pm.Remove("txFee");
}
continue;
}
if (prop.Value is JObject o)
{
o.ConvertNumberToString("rate");
if (o["paymentMethod"] is JObject pm)
{
if (pm["networkFeeRate"] is null)
pm["networkFeeRate"] = o["feeRate"] ?? 0.0m;
if (pm["networkFeeMode"] is JValue { Type: JTokenType.Integer, Value: 0 or 0L })
pm.Remove("networkFeeMode");
if (pm["networkFeeMode"] is JValue { Type: JTokenType.Integer, Value: 2 or 2L })
pm["networkFeeRate"] = 0.0m;
}
}
}
var metadata = blob.Property("metadata")?.Value as JObject;
if (metadata is null)
{
metadata = new JObject();
blob.Add("metadata", metadata);
}
foreach (var prop in (blob["buyerInformation"] as JObject)?.Properties()?.ToList() ?? [])
{
if (prop.Value?.Value<string>() is not null)
blob.Move(["buyerInformation", prop.Name], ["metadata", prop.Name]);
}
foreach (var prop in (blob["productInformation"] as JObject)?.Properties()?.ToList() ?? [])
{
if (prop.Name is "price" or "currency")
blob.Move(["productInformation", prop.Name], [prop.Name]);
else if (prop.Value?.Value<string>() is not null)
blob.Move(["productInformation", prop.Name], ["metadata", prop.Name]);
}
blob.Move(["orderId"], ["metadata", "orderId"]);
foreach (string prop in new string[] { "posData", "checkoutType", "defaultLanguage", "notificationEmail", "notificationURL", "storeSupportUrl", "redirectURL" })
{
blob.RemoveIfNull(prop);
}
blob.RemoveIfValue<bool>("fullNotifications", false);
if (blob["receiptOptions"] is JObject receiptOptions)
{
foreach (string prop in new string[] { "showQR", "enabled", "showPayments" })
{
receiptOptions.RemoveIfNull(prop);
}
}
{
if (blob.Property("paymentTolerance") is JProperty { Value: { Type: JTokenType.Float } pv } prop)
{
if (pv.Value<decimal>() == 0.0m)
prop.Remove();
}
}
var posData = blob.Move(["posData"], ["metadata", "posData"]);
if (posData is not null && posData.Value?.Type is JTokenType.String)
{
try
{
posData.Value = JObject.Parse(posData.Value<string>());
}
catch
{
posData.Remove();
}
}
if (posData?.Type is JTokenType.Null)
posData.Remove();
if (blob["derivationStrategies"] is JValue { Type: JTokenType.String } v)
blob["derivationStrategies"] = JObject.Parse(v.Value<string>());
if (blob["derivationStrategies"] is JObject derivations)
{
foreach (var prop in derivations.Properties().ToList())
{
// We should only change data for onchain
if (prop.Name.Contains('_', StringComparison.OrdinalIgnoreCase))
continue;
if (prop.Value is JValue
{
Type: JTokenType.String,
Value: String { Length: > 0 } val
})
{
if (val[0] == '{')
derivations[prop.Name] = JObject.Parse(val);
else
{
if (val.Contains('-', StringComparison.OrdinalIgnoreCase))
derivations[prop.Name] = new JObject() { ["accountDerivation"] = val };
else
derivations[prop.Name] = null;
}
}
if (prop.Value is JObject derivation)
{
derivations[prop.Name] = derivation["accountDerivation"];
}
}
}
if (blob["derivationStrategies"] is null && blob["derivationStrategy"] is not null)
{
// If it's NBX derivation strategy, keep it. Else just give up, it might be Electrum format and we shouldn't support
// that anymore in the backend for long...
if (blob["derivationStrategy"]?.Value<string>().Contains('-', StringComparison.OrdinalIgnoreCase) is true)
blob.Move(["derivationStrategy"], ["derivationStrategies", "BTC"]);
else
{
blob.Remove("derivationStrategy");
blob.Add("derivationStrategies", new JObject() { ["BTC"] = null });
}
}
if (blob["type"]?.Value<string>() is "Standard")
blob.Remove("type");
foreach (var prop in new string[] { "extendedNotifications", "lazyPaymentMethods", "lazyPaymentMethods", "redirectAutomatically" })
{
if (blob[prop]?.Value<bool>() is false)
blob.Remove(prop);
}
blob.ConvertNumberToString("price");
Currency = blob["currency"].Value<string>();
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 })
blob.Remove("speedPolicy");
blob.TryAdd("internalTags", new JArray());
blob.TryAdd("receiptOptions", new JObject());
foreach (var prop in ((JObject)blob["cryptoData"]).Properties())
{
if (prop.Name.EndsWith("_LightningLike", StringComparison.OrdinalIgnoreCase) ||
prop.Name.EndsWith("_LNURLPAY", StringComparison.OrdinalIgnoreCase))
{
if (prop.Value["paymentMethod"]?["PaymentHash"] is JObject)
prop.Value["paymentMethod"]["PaymentHash"] = JValue.CreateNull();
if (prop.Value["paymentMethod"]?["Preimage"] is JObject)
prop.Value["paymentMethod"]["Preimage"] = JValue.CreateNull();
}
}
foreach (var prop in ((JObject)blob["cryptoData"]).Properties())
{
var crypto = prop.Name.Split(['_', '-']).First();
if (blob.Move(["cryptoData", prop.Name, "rate"], ["rates", crypto]) is not null)
((JObject)blob["rates"]).ConvertNumberToString(crypto);
}
blob.Move(["cryptoData"], ["prompts"]);
var prompts = ((JObject)blob["prompts"]);
foreach (var prop in prompts.Properties().ToList())
{
((JObject)blob["prompts"]).RenameProperty(prop.Name, MigrationExtensions.MigratePaymentMethodId(prop.Name));
}
blob["derivationStrategies"] = blob["derivationStrategies"] ?? new JObject();
foreach (var prop in ((JObject)blob["derivationStrategies"]).Properties().ToList())
{
((JObject)blob["derivationStrategies"]).RenameProperty(prop.Name, MigrationExtensions.MigratePaymentMethodId(prop.Name));
}
foreach (var prop in prompts.Properties())
{
var prompt = prop.Value as JObject;
if (prompt is null)
continue;
prompt["currency"] = prop.Name.Split('-').First();
prompt.RemoveIfNull("depositAddress");
prompt.RemoveIfNull("txFee");
prompt.RemoveIfNull("feeRate");
prompt.RenameProperty("depositAddress", "destination");
prompt.RenameProperty("txFee", "paymentMethodFee");
var divisibility = MigrationExtensions.GetDivisibility(prop.Name);
prompt.Add("divisibility", divisibility);
if (prompt["paymentMethodFee"] is { Type: JTokenType.Integer } paymentMethodFee)
{
prompt["paymentMethodFee"] = ((decimal)paymentMethodFee.Value<long>() / (decimal)Math.Pow(10, divisibility)).ToString(CultureInfo.InvariantCulture);
prompt.RemoveIfValue<string>("paymentMethodFee", "0");
}
prompt.Move(["paymentMethod"], ["details"]);
prompt.Move(["feeRate"], ["details", "recommendedFeeRate"]);
prompt.Move(["details", "networkFeeRate"], ["details", "paymentMethodFeeRate"]);
prompt.Move(["details", "networkFeeMode"], ["details", "feeMode"]);
if ((prompt["details"]?["Activated"])?.Value<bool>() is bool activated)
{
((JObject)prompt["details"]).Remove("Activated");
prompt["inactive"] = !activated;
prompt.RemoveIfValue<bool>("inactive", false);
}
if ((prompt["details"]?["activated"])?.Value<bool>() is bool activated2)
{
((JObject)prompt["details"]).Remove("activated");
prompt["inactive"] = !activated2;
prompt.RemoveIfValue<bool>("inactive", false);
}
var details = prompt["details"] as JObject ?? new JObject();
details.RemoveIfValue<bool>("payjoinEnabled", false);
details.RemoveIfNull("feeMode");
if (details["feeMode"] is not (null or { Type: JTokenType.Null }))
{
details["feeMode"] = details["feeMode"].Value<int>() switch
{
1 => "Always",
2 => "Never",
_ => null
};
details.RemoveIfNull("feeMode");
}
details.RemoveIfNull("BOLT11");
details.RemoveIfNull("address");
details.RemoveIfNull("Address");
prompt.Move(["details", "BOLT11"], ["destination"]);
prompt.Move(["details", "address"], ["destination"]);
prompt.Move(["details", "Address"], ["destination"]);
prompt.RenameProperty("Address", "destination");
prompt.RenameProperty("BOLT11", "destination");
details.Remove("LightningSupportedPaymentMethod");
foreach (var o in detailsRemoveDefault)
details.RemoveIfNull(o);
details.RemoveIfValue<decimal>("recommendedFeeRate", 0.0m);
details.RemoveIfValue<decimal>("paymentMethodFeeRate", 0.0m);
if (prop.Name.EndsWith("-CHAIN"))
blob.Move(["derivationStrategies", prop.Name], ["prompts", prop.Name, "details", "accountDerivation"]);
var camel = new CamelCaseNamingStrategy();
foreach (var p in details.Properties().ToList())
{
var camelName = camel.GetPropertyName(p.Name, false);
if (camelName != p.Name)
details.RenameProperty(p.Name, camelName);
}
}
if (blob["defaultPaymentMethod"] is not (null or { Type : JTokenType.Null }))
blob["defaultPaymentMethod"] = MigrationExtensions.MigratePaymentMethodId(blob["defaultPaymentMethod"].Value<string>());
blob.Remove("derivationStrategies");
blob["version"] = 3;
Blob2 = blob.ToString(Formatting.None);
}
static string[] detailsRemoveDefault =
[
"paymentMethodFeeRate",
"keyPath",
"BOLT11",
"NodeInfo",
"Preimage",
"InvoiceId",
"PaymentHash",
"ProvidedComment",
"GeneratedBoltAmount",
"ConsumedLightningAddress",
"PayRequest"
];
#pragma warning restore CS0618 // Type or member is obsolete
}
}

View File

@ -1,16 +1,16 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
namespace BTCPayServer.Data
{
public class InvoiceData : IHasBlobUntyped
public partial class InvoiceData : IHasBlobUntyped
{
public string Id { get; set; }
public string Currency { get; set; }
public decimal? Amount { get; set; }
public string StoreDataId { get; set; }
public StoreData StoreData { get; set; }
@ -25,6 +25,7 @@ namespace BTCPayServer.Data
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; }
@ -43,12 +44,14 @@ namespace BTCPayServer.Data
builder.Entity<InvoiceData>().HasIndex(o => o.StoreDataId);
builder.Entity<InvoiceData>().HasIndex(o => o.OrderId);
builder.Entity<InvoiceData>().HasIndex(o => o.Created);
if (databaseFacade.IsNpgsql())
{
builder.Entity<InvoiceData>()
.Property(o => o.Blob2)
.HasColumnType("JSONB");
builder.Entity<InvoiceData>()
.Property(o => o.Amount)
.HasColumnType("NUMERIC");
}
}
}

View File

@ -0,0 +1,151 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO.Compression;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
namespace BTCPayServer.Data
{
public static class MigrationExtensions
{
public static JProperty? Move(this JObject blob, string[] pathFrom, string[] pathTo)
{
var from = GetProperty(blob, pathFrom, false);
if (from is null)
return null;
var to = GetProperty(blob, pathTo, true);
to!.Value = from.Value;
from.Remove();
return to;
}
public static void RenameProperty(this JObject o, string oldName, string newName)
{
var p = o.Property(oldName);
if (p is null)
return;
RenameProperty(ref p, newName);
}
public static void RenameProperty(ref JProperty ls, string newName)
{
if (ls.Name != newName)
{
var parent = ls.Parent;
ls.Remove();
ls = new JProperty(newName, ls.Value);
parent!.Add(ls);
}
}
public static JProperty? GetProperty(this JObject blob, string[] pathFrom, bool createIfNotExists)
{
var current = blob;
for (int i = 0; i < pathFrom.Length - 1; i++)
{
if (current.TryGetValue(pathFrom[i], out var value) && value is JObject jObject)
{
current = jObject;
}
else
{
if (!createIfNotExists)
return null;
JProperty? prop = null;
for (int ii = i; ii < pathFrom.Length; ii++)
{
var newProp = new JProperty(pathFrom[ii], new JObject());
if (prop is null)
current.Add(newProp);
else
prop.Value = new JObject(newProp);
prop = newProp;
}
return prop;
}
}
var result = current.Property(pathFrom[pathFrom.Length - 1]);
if (result is null && createIfNotExists)
{
result = new JProperty(pathFrom[pathFrom.Length - 1], null as object);
current.Add(result);
}
return result;
}
public static CamelCaseNamingStrategy Camel = new CamelCaseNamingStrategy();
public static void RemoveIfNull(this JObject blob, string propName)
{
if (blob.Property(propName)?.Value.Type is JTokenType.Null)
blob.Remove(propName);
}
public static void RemoveIfValue<T>(this JObject conf, string propName, T v)
{
var p = conf.Property(propName);
if (p is null)
return;
if (p.Value is JValue { Type: JTokenType.Null })
{
if (EqualityComparer<T>.Default.Equals(default, v))
p.Remove();
}
else if (p.Value is JValue jv)
{
if (EqualityComparer<T>.Default.Equals(jv.Value<T>(), v))
{
p.Remove();
}
}
}
public static void ConvertNumberToString(this JObject o, string prop)
{
if (o[prop]?.Type is JTokenType.Float)
o[prop] = o[prop]!.Value<decimal>().ToString(CultureInfo.InvariantCulture);
if (o[prop]?.Type is JTokenType.Integer)
o[prop] = o[prop]!.Value<long>().ToString(CultureInfo.InvariantCulture);
}
public static string Unzip(byte[] bytes)
{
MemoryStream ms = new MemoryStream(bytes);
using GZipStream gzip = new GZipStream(ms, CompressionMode.Decompress);
StreamReader reader = new StreamReader(gzip, Encoding.UTF8);
var unzipped = reader.ReadToEnd();
return unzipped;
}
public static int GetDivisibility(string paymentMethodId)
{
var splitted = paymentMethodId.Split('-');
return (CryptoCode: splitted[0], Type: splitted[1]) switch
{
{ Type: "LN" } or { Type: "LNURL" } => 11,
{ Type: "CHAIN", CryptoCode: var code } when code == "XMR" => 12,
{ Type: "CHAIN" } => 8,
_ => 8
};
}
public static string MigratePaymentMethodId(string paymentMethodId)
{
var splitted = paymentMethodId.Split(new[] { '_', '-' });
if (splitted is [var cryptoCode, var paymentType])
{
return paymentType switch
{
"BTCLike" => $"{cryptoCode}-CHAIN",
"LightningLike" or "LightningNetwork" => $"{cryptoCode}-LN",
"LNURLPAY" => $"{cryptoCode}-LNURL",
_ => throw new NotSupportedException("Unknown payment type " + paymentType)
};
}
if (splitted.Length == 1)
return $"{splitted[0]}-CHAIN";
throw new NotSupportedException("Unknown payment id " + paymentMethodId);
}
}
}

View File

@ -0,0 +1,179 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection.Metadata;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Migrations;
using NBitcoin;
using NBitcoin.Altcoins;
using NBitcoin.DataEncoders;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Data
{
public partial class PaymentData
{
public void Migrate()
{
#pragma warning disable CS0618 // Type or member is obsolete
if (Currency is not null)
return;
if (Blob is not (null or { Length: 0 }))
{
Blob2 = MigrationExtensions.Unzip(Blob);
Blob = null;
}
var blob = JObject.Parse(Blob2);
if (blob["cryptoPaymentDataType"] is null)
blob["cryptoPaymentDataType"] = "BTCLike";
if (blob["cryptoCode"] is null)
blob["cryptoCode"] = "BTC";
if (blob["receivedTime"] is null)
blob.Move(["receivedTimeMs"], ["receivedTime"]);
else
{
// Convert number of seconds to number of milliseconds
var timeSeconds = (ulong)(long)blob["receivedTime"].Value<long>();
var date = NBitcoin.Utils.UnixTimeToDateTime(timeSeconds);
blob["receivedTime"] = DateTimeToMilliUnixTime(date.UtcDateTime);
}
var cryptoCode = blob["cryptoCode"].Value<string>();
Type = cryptoCode + "_" + blob["cryptoPaymentDataType"].Value<string>();
Type = MigrationExtensions.MigratePaymentMethodId(Type);
var divisibility = MigrationExtensions.GetDivisibility(Type);
Currency = blob["cryptoCode"].Value<string>();
blob.Remove("cryptoCode");
blob.Remove("cryptoPaymentDataType");
JObject cryptoData;
if (blob["cryptoPaymentData"] is null)
{
cryptoData = new JObject();
blob["cryptoPaymentData"] = cryptoData;
cryptoData["RBF"] = true;
cryptoData["confirmationCount"] = 0;
}
else
{
cryptoData = JObject.Parse(blob["cryptoPaymentData"].Value<string>());
foreach (var prop in cryptoData.Properties().ToList())
{
if (prop.Name is "rbf")
cryptoData.RenameProperty("rbf", "RBF");
else if (prop.Name is "bolT11")
cryptoData.RenameProperty("bolT11", "BOLT11");
else
cryptoData.RenameProperty(prop.Name, MigrationExtensions.Camel.GetPropertyName(prop.Name, false));
}
}
blob.Remove("cryptoPaymentData");
cryptoData["outpoint"] = blob["outpoint"];
if (blob["output"] is not (null or { Type: JTokenType.Null }))
{
// Old versions didn't track addresses, so we take it from output.
// We don't know the network for sure but better having something than nothing in destination.
// If signet/testnet crash we don't really care anyway.
// Also, only LTC was supported at this time.
Network network = (cryptoCode switch { "LTC" => (INetworkSet)Litecoin.Instance, _ => Bitcoin.Instance }).Mainnet;
var txout = network.Consensus.ConsensusFactory.CreateTxOut();
txout.ReadWrite(Encoders.Hex.DecodeData(blob["output"].Value<string>()), network);
cryptoData["value"] = txout.Value.Satoshi;
blob["destination"] = txout.ScriptPubKey.GetDestinationAddress(network)?.ToString();
}
blob.Remove("output");
blob.Remove("outpoint");
// Convert from sats to btc
if (cryptoData["value"] is not (null or { Type: JTokenType.Null }))
{
var v = cryptoData["value"].Value<long>();
Amount = (decimal)v / (decimal)Money.COIN;
cryptoData.Remove("value");
blob["paymentMethodFee"] = blob["networkFee"];
blob.RemoveIfValue<decimal>("paymentMethodFee", 0.0m);
blob.ConvertNumberToString("paymentMethodFee");
blob.Remove("networkFee");
blob.RemoveIfNull("paymentMethodFee");
}
// Convert from millisats to btc
else if (cryptoData["amount"] is not (null or { Type: JTokenType.Null }))
{
var v = cryptoData["amount"].Value<long>();
Amount = (decimal)v / (decimal)Math.Pow(10.0, divisibility);
cryptoData.Remove("amount");
}
if (cryptoData["address"] is not (null or { Type: JTokenType.Null }))
{
blob["destination"] = cryptoData["address"];
cryptoData.Remove("address");
}
if (cryptoData["BOLT11"] is not (null or { Type: JTokenType.Null }))
{
blob["destination"] = cryptoData["BOLT11"];
cryptoData.Remove("BOLT11");
}
if (cryptoData["outpoint"] is not (null or { Type: JTokenType.Null }))
{
// Convert to format txid-n
cryptoData["outpoint"] = OutPoint.Parse(cryptoData["outpoint"].Value<string>()).ToString();
}
if (Accounted is false)
Status = PaymentStatus.Unaccounted;
else if (cryptoData["confirmationCount"] is { Type: JTokenType.Integer })
{
var confirmationCount = cryptoData["confirmationCount"].Value<int>();
// Technically, we should use the invoice's speed policy, however it's not on our
// scope and is good enough for majority of cases.
Status = confirmationCount > 0 ? PaymentStatus.Settled : PaymentStatus.Processing;
if (cryptoData["LockTime"] is { Type: JTokenType.Integer })
{
var lockTime = cryptoData["LockTime"].Value<int>();
if (confirmationCount < lockTime)
Status = PaymentStatus.Processing;
}
}
else
{
Status = PaymentStatus.Settled;
}
Created = MilliUnixTimeToDateTime(blob["receivedTime"].Value<long>());
cryptoData.RemoveIfValue<bool>("rbf", false);
cryptoData.Remove("legacy");
cryptoData.Remove("networkFee");
cryptoData.Remove("paymentType");
cryptoData.RemoveIfNull("outpoint");
cryptoData.RemoveIfValue<bool>("RBF", false);
blob.Remove("receivedTime");
blob.Remove("accounted");
blob.Remove("networkFee");
blob["details"] = cryptoData;
blob["divisibility"] = divisibility;
blob["version"] = 2;
Blob2 = blob.ToString(Formatting.None);
Accounted = null;
#pragma warning restore CS0618 // Type or member is obsolete
}
static readonly DateTimeOffset unixRef = new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero);
public static long DateTimeToMilliUnixTime(in DateTime time)
{
var date = ((DateTimeOffset)time).ToUniversalTime();
long v = (long)(date - unixRef).TotalMilliseconds;
if (v < 0)
throw new FormatException("Invalid datetime (less than 1/1/1970)");
return v;
}
public static DateTimeOffset MilliUnixTimeToDateTime(long value)
{
var v = value;
if (v < 0)
throw new FormatException("Invalid datetime (less than 1/1/1970)");
return unixRef + TimeSpan.FromMilliseconds(v);
}
}
}

View File

@ -4,17 +4,32 @@ using Microsoft.EntityFrameworkCore.Infrastructure;
namespace BTCPayServer.Data
{
public class PaymentData : IHasBlobUntyped
public enum PaymentStatus
{
Processing,
Settled,
Unaccounted
}
public partial class PaymentData : IHasBlobUntyped
{
/// <summary>
/// The date of creation of the payment
/// Note that while it is a nullable field, our migration
/// process ensure it is populated.
/// </summary>
public DateTimeOffset? Created { get; set; }
public string Id { get; set; }
public string InvoiceDataId { get; set; }
public string Currency { get; set; }
public decimal? Amount { get; set; }
public InvoiceData InvoiceData { get; set; }
[Obsolete("Use Blob2 instead")]
public byte[] Blob { get; set; }
public string Blob2 { get; set; }
public string Type { get; set; }
public bool Accounted { get; set; }
[Obsolete("Use Status instead")]
public bool? Accounted { get; set; }
public PaymentStatus? Status { get; set; }
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
{
@ -23,11 +38,17 @@ namespace BTCPayServer.Data
.WithMany(i => i.Payments).OnDelete(DeleteBehavior.Cascade);
builder.Entity<PaymentData>()
.HasIndex(o => o.InvoiceDataId);
builder.Entity<PaymentData>()
.Property(o => o.Status)
.HasConversion<string>();
if (databaseFacade.IsNpgsql())
{
builder.Entity<PaymentData>()
.Property(o => o.Blob2)
.HasColumnType("JSONB");
builder.Entity<PaymentData>()
.Property(o => o.Amount)
.HasColumnType("NUMERIC");
}
}
}

View File

@ -5,6 +5,7 @@ using BTCPayServer.Client.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Data
{
@ -44,7 +45,6 @@ namespace BTCPayServer.Data
public IEnumerable<LightningAddressData> LightningAddresses { get; set; }
public IEnumerable<PayoutProcessorData> PayoutProcessors { get; set; }
public IEnumerable<PayoutData> Payouts { get; set; }
public IEnumerable<CustodianAccountData> CustodianAccounts { get; set; }
public IEnumerable<StoreSettingData> Settings { get; set; }
public IEnumerable<FormData> Forms { get; set; }
public IEnumerable<StoreRole> StoreRoles { get; set; }

View File

@ -0,0 +1,43 @@
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace BTCPayServer.Migrations
{
/// <inheritdoc />
[DbContext(typeof(ApplicationDbContext))]
[Migration("20240304003640_addinvoicecolumns")]
public partial class addinvoicecolumns : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<decimal>(
name: "Amount",
table: "Invoices",
type: migrationBuilder.IsNpgsql() ? "NUMERIC" : "TEXT",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Currency",
table: "Invoices",
type: "TEXT",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Amount",
table: "Invoices");
migrationBuilder.DropColumn(
name: "Currency",
table: "Invoices");
}
}
}

View File

@ -0,0 +1,69 @@
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("20240317024757_payments_refactor")]
public partial class payments_refactor : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<decimal>(
name: "Amount",
table: "Payments",
type: migrationBuilder.IsNpgsql() ? "NUMERIC" : "TEXT",
nullable: true);
migrationBuilder.AddColumn<DateTimeOffset>(
name: "Created",
table: "Payments",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Currency",
table: "Payments",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Status",
table: "Payments",
type: "TEXT",
nullable: true);
if (migrationBuilder.IsNpgsql())
{
migrationBuilder.AlterColumn<bool?>(
name: "Accounted",
table: "Payments",
nullable: true);
}
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Amount",
table: "Payments");
migrationBuilder.DropColumn(
name: "Created",
table: "Payments");
migrationBuilder.DropColumn(
name: "Currency",
table: "Payments");
migrationBuilder.DropColumn(
name: "Status",
table: "Payments");
}
}
}

View File

@ -0,0 +1,54 @@
using System;
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace BTCPayServer.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20240325095923_RemoveCustodian")]
public partial class RemoveCustodian : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "CustodianAccount");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "CustodianAccount",
columns: table => new
{
Id = table.Column<string>(type: "TEXT", maxLength: 50, nullable: false),
StoreId = table.Column<string>(type: "TEXT", maxLength: 50, nullable: false),
Blob = table.Column<byte[]>(type: "BLOB", nullable: true),
Blob2 = table.Column<string>(type: "TEXT", nullable: true),
CustodianCode = table.Column<string>(type: "TEXT", maxLength: 50, nullable: false),
Name = table.Column<string>(type: "TEXT", maxLength: 50, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_CustodianAccount", x => x.Id);
table.ForeignKey(
name: "FK_CustodianAccount_Stores_StoreId",
column: x => x.StoreId,
principalTable: "Stores",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_CustodianAccount_StoreId",
table: "CustodianAccount",
column: "StoreId");
}
}
}

View File

@ -1,4 +1,4 @@
// <auto-generated />
// <auto-generated />
using System;
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore;
@ -189,40 +189,6 @@ namespace BTCPayServer.Migrations
b.ToTable("AspNetUsers", (string)null);
});
modelBuilder.Entity("BTCPayServer.Data.CustodianAccountData", b =>
{
b.Property<string>("Id")
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<byte[]>("Blob")
.HasColumnType("BLOB");
b.Property<string>("Blob2")
.HasColumnType("TEXT");
b.Property<string>("CustodianCode")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<string>("StoreId")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("StoreId");
b.ToTable("CustodianAccount");
});
modelBuilder.Entity("BTCPayServer.Data.Fido2Credential", b =>
{
b.Property<string>("Id")
@ -281,6 +247,9 @@ namespace BTCPayServer.Migrations
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<decimal?>("Amount")
.HasColumnType("TEXT");
b.Property<bool>("Archived")
.HasColumnType("INTEGER");
@ -293,6 +262,9 @@ namespace BTCPayServer.Migrations
b.Property<DateTimeOffset>("Created")
.HasColumnType("TEXT");
b.Property<string>("Currency")
.HasColumnType("TEXT");
b.Property<string>("CustomerEmail")
.HasColumnType("TEXT");
@ -539,15 +511,27 @@ namespace BTCPayServer.Migrations
b.Property<bool>("Accounted")
.HasColumnType("INTEGER");
b.Property<decimal?>("Amount")
.HasColumnType("TEXT");
b.Property<byte[]>("Blob")
.HasColumnType("BLOB");
b.Property<string>("Blob2")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("Created")
.HasColumnType("TEXT");
b.Property<string>("Currency")
.HasColumnType("TEXT");
b.Property<string>("InvoiceDataId")
.HasColumnType("TEXT");
b.Property<string>("Status")
.HasColumnType("TEXT");
b.Property<string>("Type")
.HasColumnType("TEXT");
@ -598,7 +582,7 @@ namespace BTCPayServer.Migrations
.HasMaxLength(30)
.HasColumnType("TEXT");
b.Property<byte[]>("Blob")
b.Property<string>("Blob")
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("Date")
@ -612,7 +596,7 @@ namespace BTCPayServer.Migrations
.HasMaxLength(20)
.HasColumnType("TEXT");
b.Property<byte[]>("Proof")
b.Property<string>("Proof")
.HasColumnType("TEXT");
b.Property<string>("PullPaymentDataId")
@ -703,7 +687,7 @@ namespace BTCPayServer.Migrations
b.Property<bool>("Archived")
.HasColumnType("INTEGER");
b.Property<byte[]>("Blob")
b.Property<string>("Blob")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("EndDate")
@ -1219,17 +1203,6 @@ namespace BTCPayServer.Migrations
b.Navigation("StoreData");
});
modelBuilder.Entity("BTCPayServer.Data.CustodianAccountData", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany("CustodianAccounts")
.HasForeignKey("StoreId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("StoreData");
});
modelBuilder.Entity("BTCPayServer.Data.Fido2Credential", b =>
{
b.HasOne("BTCPayServer.Data.ApplicationUser", "ApplicationUser")
@ -1638,8 +1611,6 @@ namespace BTCPayServer.Migrations
b.Navigation("Apps");
b.Navigation("CustodianAccounts");
b.Navigation("Forms");
b.Navigation("Invoices");

View File

@ -17,6 +17,7 @@ using BTCPayServer.Plugins.PointOfSale;
using BTCPayServer.Plugins.PointOfSale.Controllers;
using BTCPayServer.Plugins.PointOfSale.Models;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices;
using Microsoft.AspNetCore.Mvc;
using NBitcoin;
using NBitpayClient;
@ -172,12 +173,13 @@ namespace BTCPayServer.Tests
// Now let's check that no data has been lost in the process
var store = tester.PayTester.StoreRepository.FindStore(storeId).GetAwaiter().GetResult();
var onchainBTC = store.GetSupportedPaymentMethods(tester.PayTester.Networks)
#pragma warning disable CS0618 // Type or member is obsolete
.OfType<DerivationSchemeSettings>().First(o => o.PaymentId.IsBTCOnChain);
#pragma warning restore CS0618 // Type or member is obsolete
FastTests.GetParsers().TryParseWalletFile(content, onchainBTC.Network, out var expected, out var error);
Assert.Equal(expected.ToJson(), onchainBTC.ToJson());
var handlers = tester.PayTester.GetService<PaymentMethodHandlerDictionary>();
var pmi = PaymentTypes.CHAIN.GetPaymentMethodId("BTC");
var onchainBTC = store.GetPaymentMethodConfig<DerivationSchemeSettings>(pmi, handlers);
var network = handlers.GetBitcoinHandler("BTC").Network;
FastTests.GetParsers().TryParseWalletFile(content, network, out var expected, out var error);
var handler = handlers[pmi];
Assert.Equal(JToken.FromObject(expected, handler.Serializer), JToken.FromObject(onchainBTC, handler.Serializer));
Assert.Null(error);
// Let's check that the root hdkey and account key path are taken into account when making a PSBT
@ -302,6 +304,7 @@ namespace BTCPayServer.Tests
var cashCow = tester.LTCExplorerNode;
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network);
var firstPayment = Money.Coins(0.1m);
var firstDue = invoice.CryptoInfo[0].Due;
cashCow.SendToAddress(invoiceAddress, firstPayment);
TestUtils.Eventually(() =>
{
@ -381,7 +384,7 @@ namespace BTCPayServer.Tests
await TestUtils.EventuallyAsync(async () =>
{
invoice = await user.BitPay.GetInvoiceAsync(invoice.Id);
Assert.Equal("confirmed", invoice.Status);
Assert.Equal("complete", invoice.Status);
});
// BTC crash by 50%
@ -829,13 +832,13 @@ normal:
Assert.Single(btcOnlyInvoice.CryptoInfo);
Assert.Equal("BTC",
btcOnlyInvoice.CryptoInfo.First().CryptoCode);
Assert.Equal(PaymentTypes.BTCLike.ToString(),
Assert.Equal("BTC-CHAIN",
btcOnlyInvoice.CryptoInfo.First().PaymentType);
Assert.Equal(2, normalInvoice.CryptoInfo.Length);
Assert.Contains(
normalInvoice.CryptoInfo,
s => PaymentTypes.BTCLike.ToString() == s.PaymentType && new[] { "BTC", "LTC" }.Contains(
s => "BTC-CHAIN" == s.PaymentType && new[] { "BTC", "LTC" }.Contains(
s.CryptoCode));
//test topup option

View File

@ -19,6 +19,14 @@
<DefineConstants>$(DefineConstants);SHORT_TIMEOUT</DefineConstants>
</PropertyGroup>
<ItemGroup>
<None Remove="TestData\OldInvoices.csv" />
</ItemGroup>
<ItemGroup>
<Content Include="TestData\OldInvoices.csv" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.15" />

View File

@ -7,13 +7,11 @@ using System.Security.Claims;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Custodians;
using BTCPayServer.Configuration;
using BTCPayServer.HostedServices;
using BTCPayServer.Hosting;
using BTCPayServer.Rating;
using BTCPayServer.Services;
using BTCPayServer.Services.Custodian.Client.MockCustodian;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
@ -193,7 +191,6 @@ namespace BTCPayServer.Tests
.ConfigureServices(services =>
{
services.TryAddSingleton<IFeeProviderFactory>(new BTCPayServer.Services.Fees.FixedFeeProvider(new FeeRate(100L, 1)));
services.AddSingleton<ICustodian, MockCustodian>();
})
.UseKestrel()
.UseStartup<Startup>()

View File

@ -162,9 +162,9 @@ namespace BTCPayServer.Tests
s.AddLightningNode();
s.AddDerivationScheme();
var invoiceId = s.CreateInvoice(defaultPaymentMethod: "BTC_LightningLike");
var invoiceId = s.CreateInvoice(defaultPaymentMethod: "BTC-LN");
s.GoToInvoiceCheckout(invoiceId);
Assert.Equal("Bitcoin (Lightning)", s.Driver.FindElement(By.ClassName("payment__currencies")).Text);
Assert.Equal("Lightning", s.Driver.FindElement(By.ClassName("payment__currencies")).Text);
s.Driver.Quit();
}
@ -210,8 +210,8 @@ namespace BTCPayServer.Tests
Assert.True(s.Driver.FindElement(By.Name("btcpay")).Displayed);
});
await s.Server.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create(invoice
.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike))
.GetPaymentMethodDetails().GetPaymentDestination(), Network.RegTest),
.GetPaymentPrompt(PaymentTypes.CHAIN.GetPaymentMethodId("BTC"))
.Destination, Network.RegTest),
new Money(0.001m, MoneyUnit.BTC));
IWebElement closebutton = null;

View File

@ -62,13 +62,13 @@ 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 .truncate-center-start")).Text;
Assert.StartsWith("bcrt", s.Driver.FindElement(By.CssSelector("#Address_BTC .truncate-center-start")).Text);
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);
Assert.DoesNotContain("lightning=", payUrl);
Assert.Equal($"bitcoin:{address}", payUrl);
Assert.Equal($"bitcoin:{address}", clipboard);
Assert.Equal($"bitcoin:{address.ToUpperInvariant()}", qrValue);
s.Driver.ElementDoesNotExist(By.Id("Lightning_BTC"));
s.Driver.ElementDoesNotExist(By.Id("Lightning_BTC-CHAIN"));
// Details should show exchange rate
s.Driver.ToggleCollapse("PaymentDetails");
@ -84,13 +84,13 @@ 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 .truncate-center-start")).Text);
s.Driver.ElementDoesNotExist(By.Id("Address_BTC"));
Assert.StartsWith("lnurl", s.Driver.WaitForElement(By.CssSelector("#Lightning_BTC-CHAIN .truncate-center-start")).Text);
s.Driver.ElementDoesNotExist(By.Id("Address_BTC-CHAIN"));
});
// Default payment method
s.GoToHome();
invoiceId = s.CreateInvoice(21000, "SATS", defaultPaymentMethod: "BTC_LightningLike");
invoiceId = s.CreateInvoice(21000, "SATS", defaultPaymentMethod: "BTC-LN");
s.GoToInvoiceCheckout(invoiceId);
s.Driver.WaitUntilAvailable(By.Id("Checkout-v2"));
Assert.Equal(2, s.Driver.FindElements(By.CssSelector(".payment-method")).Count);
@ -99,11 +99,11 @@ 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_LightningLike .truncate-center-start")).Text;
address = s.Driver.FindElement(By.CssSelector("#Lightning_BTC-LN .truncate-center-start")).Text;
Assert.Equal($"lightning:{address}", payUrl);
Assert.Equal($"lightning:{address}", clipboard);
Assert.Equal($"lightning:{address.ToUpperInvariant()}", qrValue);
s.Driver.ElementDoesNotExist(By.Id("Address_BTC"));
s.Driver.ElementDoesNotExist(By.Id("Address_BTC-CHAIN"));
// Lightning amount in sats
Assert.Contains("BTC", s.Driver.FindElement(By.Id("AmountDue")).Text);
@ -155,7 +155,7 @@ namespace BTCPayServer.Tests
s.Driver.WaitUntilAvailable(By.Id("Checkout-v2"));
await Task.Delay(200);
address = s.Driver.FindElement(By.CssSelector("#Address_BTC .truncate-center-start")).Text;
address = s.Driver.FindElement(By.CssSelector("#Address_BTC-CHAIN .truncate-center-start")).Text;
var amountFraction = "0.00001";
await s.Server.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create(address, Network.RegTest),
Money.Parse(amountFraction));
@ -271,8 +271,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 .truncate-center-start")).Text;
var copyAddressLightning = s.Driver.FindElement(By.CssSelector("#Lightning_BTC .truncate-center-start")).Text;
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;
Assert.StartsWith($"bitcoin:{copyAddressOnchain}?amount=", payUrl);
Assert.Contains("?amount=", payUrl);
Assert.Contains("&lightning=", payUrl);
@ -311,7 +311,7 @@ namespace BTCPayServer.Tests
// BIP21 with LN as default payment method
s.GoToHome();
invoiceId = s.CreateInvoice(defaultPaymentMethod: "BTC_LightningLike");
invoiceId = s.CreateInvoice(defaultPaymentMethod: "BTC-LN");
s.GoToInvoiceCheckout(invoiceId);
s.Driver.WaitUntilAvailable(By.Id("Checkout-v2"));
Assert.Empty(s.Driver.FindElements(By.CssSelector(".payment-method")));
@ -340,8 +340,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 .truncate-center-start")).Text;
copyAddressLightning = s.Driver.FindElement(By.CssSelector("#Lightning_BTC .truncate-center-start")).Text;
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;
Assert.StartsWith($"bitcoin:{copyAddressOnchain}", payUrl);
Assert.Contains("?lightning=lnurl", payUrl);
Assert.DoesNotContain("amount=", payUrl);
@ -414,7 +414,7 @@ namespace BTCPayServer.Tests
// - NFC/LNURL-W available with just Lightning
// - BIP21 works correctly even though Lightning is default payment method
s.GoToHome();
invoiceId = s.CreateInvoice(defaultPaymentMethod: "BTC_LightningLike");
invoiceId = s.CreateInvoice(defaultPaymentMethod: "BTC-LN");
s.GoToInvoiceCheckout(invoiceId);
s.Driver.WaitUntilAvailable(By.Id("Checkout-v2"));
Assert.Empty(s.Driver.FindElements(By.CssSelector(".payment-method")));
@ -462,8 +462,8 @@ namespace BTCPayServer.Tests
iframe.WaitUntilAvailable(By.Id("Checkout-v2"));
await s.Server.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create(invoice
.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike))
.GetPaymentMethodDetails().GetPaymentDestination(), Network.RegTest),
.GetPaymentPrompt(PaymentTypes.CHAIN.GetPaymentMethodId("BTC"))
.Destination, Network.RegTest),
new Money(0.001m, MoneyUnit.BTC));
TestUtils.Eventually(() =>

View File

@ -3,6 +3,7 @@ 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;
@ -47,6 +48,7 @@ using NBXplorer.DerivationStrategy;
using NBXplorer.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
using OpenQA.Selenium.DevTools.V100.DOMSnapshot;
using Xunit;
using Xunit.Abstractions;
@ -152,13 +154,6 @@ namespace BTCPayServer.Tests
CanParseDecimalsCore("{\"qty\": \"1.0\"}", 1.0m);
CanParseDecimalsCore("{\"qty\": 6.1e-7}", 6.1e-7m);
CanParseDecimalsCore("{\"qty\": \"6.1e-7\"}", 6.1e-7m);
var data = JsonConvert.DeserializeObject<TradeRequestData>("{\"qty\": \"6.1e-7\", \"fromAsset\":\"Test\"}");
Assert.Equal(6.1e-7m, data.Qty.Value);
Assert.Equal("Test", data.FromAsset);
data = JsonConvert.DeserializeObject<TradeRequestData>("{\"fromAsset\":\"Test\", \"qty\": \"6.1e-7\"}");
Assert.Equal(6.1e-7m, data.Qty.Value);
Assert.Equal("Test", data.FromAsset);
}
[Fact]
@ -202,8 +197,6 @@ namespace BTCPayServer.Tests
{
var d = JsonConvert.DeserializeObject<LedgerEntryData>(str);
Assert.Equal(expected, d.Qty);
var d2 = JsonConvert.DeserializeObject<TradeRequestData>(str);
Assert.Equal(new TradeQuantity(expected, TradeQuantity.ValueType.Exact), d2.Qty);
}
[Fact]
@ -245,26 +238,24 @@ namespace BTCPayServer.Tests
var id = PaymentMethodId.Parse("BTC");
var id1 = PaymentMethodId.Parse("BTC-OnChain");
var id2 = PaymentMethodId.Parse("BTC-BTCLike");
Assert.Equal("LTC-LN", PaymentMethodId.Parse("LTC-LightningNetwork").ToString());
Assert.Equal(id, id1);
Assert.Equal(id, id2);
Assert.Equal("BTC", id.ToString());
Assert.Equal("BTC", id.ToString());
Assert.Equal("BTC-CHAIN", id.ToString());
Assert.Equal("BTC-CHAIN", id.ToString());
id = PaymentMethodId.Parse("LTC");
Assert.Equal("LTC", id.ToString());
Assert.Equal("LTC", id.ToStringNormalized());
Assert.Equal("LTC-CHAIN", id.ToString());
id = PaymentMethodId.Parse("LTC-offchain");
id1 = PaymentMethodId.Parse("LTC-OffChain");
id2 = PaymentMethodId.Parse("LTC-LightningLike");
Assert.Equal(id, id1);
Assert.Equal(id, id2);
Assert.Equal("LTC_LightningLike", id.ToString());
Assert.Equal("LTC-LightningNetwork", id.ToStringNormalized());
Assert.Equal("LTC-LN", id.ToString());
#if ALTCOINS
id = PaymentMethodId.Parse("XMR");
id1 = PaymentMethodId.Parse("XMR-MoneroLike");
Assert.Equal(id, id1);
Assert.Equal("XMR_MoneroLike", id.ToString());
Assert.Equal("XMR", id.ToStringNormalized());
Assert.Equal("XMR-CHAIN", id.ToString());
#endif
}
@ -448,29 +439,31 @@ namespace BTCPayServer.Tests
}}
}, out items));
}
PaymentMethodId BTC = PaymentTypes.CHAIN.GetPaymentMethodId("BTC");
PaymentMethodId LTC = PaymentTypes.CHAIN.GetPaymentMethodId("LTC");
[Fact]
public void CanCalculateDust()
{
var entity = new InvoiceEntity() { Currency = "USD" };
entity.Networks = CreateNetworkProvider(ChainName.Regtest);
#pragma warning disable CS0618
entity.Payments = new System.Collections.Generic.List<PaymentEntity>();
entity.SetPaymentMethod(new PaymentMethod()
entity.Rates["BTC"] = 34_000m;
entity.Rates["LTC"] = 3400m;
entity.SetPaymentPrompt(BTC, new PaymentPrompt()
{
Currency = "BTC",
Rate = 34_000m
Divisibility = 8
});
entity.Price = 4000;
entity.UpdateTotals();
var accounting = entity.GetPaymentMethods().First().Calculate();
var accounting = entity.GetPaymentPrompts().First().Calculate();
// Exact price should be 0.117647059..., but the payment method round up to one sat
Assert.Equal(0.11764706m, accounting.Due);
entity.Payments.Add(new PaymentEntity()
{
Currency = "BTC",
Output = new TxOut(Money.Coins(0.11764706m), new Key()),
Accounted = true
Value = 0.11764706m,
Status = PaymentStatus.Settled,
});
entity.UpdateTotals();
Assert.Equal(0.0m, entity.NetDue);
@ -483,13 +476,13 @@ namespace BTCPayServer.Tests
// Now, imagine there is litecoin. It might seem from its
// perspecitve that there has been a slight over payment.
// However, Calculate() should just cap it to 0.0m
entity.SetPaymentMethod(new PaymentMethod()
entity.SetPaymentPrompt(LTC, new PaymentPrompt()
{
Currency = "LTC",
Rate = 3400m
Divisibility = 8
});
entity.UpdateTotals();
var method = entity.GetPaymentMethods().First(p => p.Currency == "LTC");
var method = entity.GetPaymentPrompts().First(p => p.Currency == "LTC");
accounting = method.Calculate();
Assert.Equal(0.0m, accounting.DueUncapped);
@ -501,19 +494,19 @@ namespace BTCPayServer.Tests
{
var networkProvider = CreateNetworkProvider(ChainName.Regtest);
var entity = new InvoiceEntity() { Currency = "USD" };
entity.Networks = networkProvider;
#pragma warning disable CS0618
entity.Payments = new System.Collections.Generic.List<PaymentEntity>();
entity.SetPaymentMethod(new PaymentMethod()
entity.Rates["BTC"] = 5000m;
entity.SetPaymentPrompt(BTC, new PaymentPrompt()
{
Currency = "BTC",
Rate = 5000,
NextNetworkFee = Money.Coins(0.1m)
PaymentMethodFee = 0.1m,
Divisibility = 8
});
entity.Price = 5000;
entity.UpdateTotals();
var paymentMethod = entity.GetPaymentMethods().TryGet("BTC", PaymentTypes.BTCLike);
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);
@ -522,10 +515,10 @@ namespace BTCPayServer.Tests
entity.Payments.Add(new PaymentEntity()
{
Currency = "BTC",
Output = new TxOut(Money.Coins(0.5m), new Key()),
Value = 0.5m,
Rate = 5000,
Accounted = true,
NetworkFee = 0.1m
Status = PaymentStatus.Settled,
PaymentMethodFee = 0.1m
});
entity.UpdateTotals();
accounting = paymentMethod.Calculate();
@ -536,9 +529,9 @@ namespace BTCPayServer.Tests
entity.Payments.Add(new PaymentEntity()
{
Currency = "BTC",
Output = new TxOut(Money.Coins(0.2m), new Key()),
Accounted = true,
NetworkFee = 0.1m
Value = 0.2m,
Status = PaymentStatus.Settled,
PaymentMethodFee = 0.1m
});
entity.UpdateTotals();
accounting = paymentMethod.Calculate();
@ -548,9 +541,9 @@ namespace BTCPayServer.Tests
entity.Payments.Add(new PaymentEntity()
{
Currency = "BTC",
Output = new TxOut(Money.Coins(0.6m), new Key()),
Accounted = true,
NetworkFee = 0.1m
Value = 0.6m,
Status = PaymentStatus.Settled,
PaymentMethodFee = 0.1m
});
entity.UpdateTotals();
accounting = paymentMethod.Calculate();
@ -558,75 +551,79 @@ namespace BTCPayServer.Tests
Assert.Equal(1.3m, accounting.TotalDue);
entity.Payments.Add(
new PaymentEntity() { Currency = "BTC", Output = new TxOut(Money.Coins(0.2m), new Key()), Accounted = true });
new PaymentEntity() { Currency = "BTC", Value = 0.2m, Status = PaymentStatus.Settled });
entity.UpdateTotals();
accounting = paymentMethod.Calculate();
Assert.Equal(0.0m, accounting.Due);
Assert.Equal(1.3m, accounting.TotalDue);
entity = new InvoiceEntity();
entity.Networks = networkProvider;
entity.Price = 5000;
PaymentMethodDictionary paymentMethods = new PaymentMethodDictionary();
paymentMethods.Add(
new PaymentMethod() { Currency = "BTC", Rate = 1000, NextNetworkFee = Money.Coins(0.1m) });
paymentMethods.Add(
new PaymentMethod() { Currency = "LTC", Rate = 500, NextNetworkFee = Money.Coins(0.01m) });
entity.SetPaymentMethods(paymentMethods);
entity.Currency = "USD";
entity.Rates["BTC"] = 1000m;
entity.Rates["LTC"] = 500m;
PaymentPromptDictionary paymentMethods =
[
new PaymentPrompt() { PaymentMethodId = BTC, Currency = "BTC", PaymentMethodFee = 0.1m, Divisibility = 8 },
new PaymentPrompt() { PaymentMethodId = LTC, Currency = "LTC", PaymentMethodFee = 0.01m, Divisibility = 8 },
];
entity.SetPaymentPrompts(paymentMethods);
entity.Payments = new List<PaymentEntity>();
entity.UpdateTotals();
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike));
paymentMethod = entity.GetPaymentPrompt(BTC);
accounting = paymentMethod.Calculate();
Assert.Equal(5.1m, accounting.Due);
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike));
paymentMethod = entity.GetPaymentPrompt(LTC);
accounting = paymentMethod.Calculate();
Assert.Equal(10.01m, accounting.TotalDue);
entity.Payments.Add(new PaymentEntity()
{
PaymentMethodId = BTC,
Currency = "BTC",
Output = new TxOut(Money.Coins(1.0m), new Key()),
Accounted = true,
NetworkFee = 0.1m
Value = 1.0m,
Status = PaymentStatus.Settled,
PaymentMethodFee = 0.1m
});
entity.UpdateTotals();
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike));
paymentMethod = entity.GetPaymentPrompt(PaymentTypes.CHAIN.GetPaymentMethodId("BTC"));
accounting = paymentMethod.Calculate();
Assert.Equal(4.2m, accounting.Due);
Assert.Equal(1.0m, accounting.CryptoPaid);
Assert.Equal(1.0m, accounting.PaymentMethodPaid);
Assert.Equal(1.0m, accounting.Paid);
Assert.Equal(5.2m, accounting.TotalDue);
Assert.Equal(2, accounting.TxRequired);
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike));
paymentMethod = entity.GetPaymentPrompt(PaymentTypes.CHAIN.GetPaymentMethodId("LTC"));
accounting = paymentMethod.Calculate();
Assert.Equal(10.01m + 0.1m * 2 - 2.0m /* 8.21m */, accounting.Due);
Assert.Equal(0.0m, accounting.CryptoPaid);
Assert.Equal(0.0m, accounting.PaymentMethodPaid);
Assert.Equal(2.0m, accounting.Paid);
Assert.Equal(10.01m + 0.1m * 2, accounting.TotalDue);
entity.Payments.Add(new PaymentEntity()
{
PaymentMethodId = LTC,
Currency = "LTC",
Output = new TxOut(Money.Coins(1.0m), new Key()),
Accounted = true,
NetworkFee = 0.01m
Value = 1.0m,
Status = PaymentStatus.Settled,
PaymentMethodFee = 0.01m
});
entity.UpdateTotals();
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike));
paymentMethod = entity.GetPaymentPrompt(PaymentTypes.CHAIN.GetPaymentMethodId("BTC"));
accounting = paymentMethod.Calculate();
Assert.Equal(4.2m - 0.5m + 0.01m / 2, accounting.Due);
Assert.Equal(1.0m, accounting.CryptoPaid);
Assert.Equal(1.0m, accounting.PaymentMethodPaid);
Assert.Equal(1.5m, accounting.Paid);
Assert.Equal(5.2m + 0.01m / 2, accounting.TotalDue); // The fee for LTC added
Assert.Equal(2, accounting.TxRequired);
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike));
paymentMethod = entity.GetPaymentPrompt(PaymentTypes.CHAIN.GetPaymentMethodId("LTC"));
accounting = paymentMethod.Calculate();
Assert.Equal(8.21m - 1.0m + 0.01m, accounting.Due);
Assert.Equal(1.0m, accounting.CryptoPaid);
Assert.Equal(1.0m, accounting.PaymentMethodPaid);
Assert.Equal(3.0m, accounting.Paid);
Assert.Equal(10.01m + 0.1m * 2 + 0.01m, accounting.TotalDue);
Assert.Equal(2, accounting.TxRequired);
@ -634,25 +631,26 @@ namespace BTCPayServer.Tests
var remaining = Money.Coins(4.2m - 0.5m + 0.01m / 2.0m).ToDecimal(MoneyUnit.BTC);
entity.Payments.Add(new PaymentEntity()
{
PaymentMethodId = BTC,
Currency = "BTC",
Output = new TxOut(Money.Coins(remaining), new Key()),
Accounted = true,
NetworkFee = 0.1m
Value = remaining,
Status = PaymentStatus.Settled,
PaymentMethodFee = 0.1m
});
entity.UpdateTotals();
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike));
paymentMethod = entity.GetPaymentPrompt(PaymentTypes.CHAIN.GetPaymentMethodId("BTC"));
accounting = paymentMethod.Calculate();
Assert.Equal(0.0m, accounting.Due);
Assert.Equal(1.0m + remaining, accounting.CryptoPaid);
Assert.Equal(1.0m + remaining, accounting.PaymentMethodPaid);
Assert.Equal(1.5m + remaining, accounting.Paid);
Assert.Equal(5.2m + 0.01m / 2, accounting.TotalDue);
Assert.Equal(accounting.Paid, accounting.TotalDue);
Assert.Equal(2, accounting.TxRequired);
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike));
paymentMethod = entity.GetPaymentPrompt(PaymentTypes.CHAIN.GetPaymentMethodId("LTC"));
accounting = paymentMethod.Calculate();
Assert.Equal(0.0m, accounting.Due);
Assert.Equal(1.0m, accounting.CryptoPaid);
Assert.Equal(1.0m, accounting.PaymentMethodPaid);
Assert.Equal(3.0m + remaining * 2, accounting.Paid);
// Paying 2 BTC fee, LTC fee removed because fully paid
Assert.Equal(10.01m + 0.1m * 2 + 0.1m * 2 /* + 0.01m no need to pay this fee anymore */,
@ -688,21 +686,21 @@ namespace BTCPayServer.Tests
public void CanAcceptInvoiceWithTolerance()
{
var networkProvider = CreateNetworkProvider(ChainName.Regtest);
var entity = new InvoiceEntity();
entity.Networks = networkProvider;
var entity = new InvoiceEntity() { Currency = "USD" };
#pragma warning disable CS0618
entity.Payments = new List<PaymentEntity>();
entity.SetPaymentMethod(new PaymentMethod()
entity.Rates["BTC"] = 5000m;
entity.SetPaymentPrompt(BTC, new PaymentPrompt()
{
Currency = "BTC",
Rate = 5000,
NextNetworkFee = Money.Coins(0.1m)
PaymentMethodFee = 0.1m,
Divisibility = 8
});
entity.Price = 5000;
entity.PaymentTolerance = 0;
entity.UpdateTotals();
var paymentMethod = entity.GetPaymentMethods().TryGet("BTC", PaymentTypes.BTCLike);
var paymentMethod = entity.GetPaymentPrompts().TryGet(PaymentTypes.CHAIN.GetPaymentMethodId("BTC"));
var accounting = paymentMethod.Calculate();
Assert.Equal(1.1m, accounting.Due);
Assert.Equal(1.1m, accounting.TotalDue);
@ -892,33 +890,6 @@ namespace BTCPayServer.Tests
Assert.Throws<ParsingException>(() => { parser.ParseOutputDescriptor("invalid"); }); // invalid in general
Assert.Throws<ParsingException>(() => { parser.ParseOutputDescriptor("wpkh([8b60afd1/49h/0h/0h]xpub661MyMwAFXkMnyoBjyHndD3QwRbcGVBsTGeNZN6QGVHcfz4MPzBUxjSevweNFQx7SqmMHLdSA4FteGsRrEriu4pnVZMZWnruFFAYZATtcDw/0/*)#9x4vkw48"); }); // invalid checksum
}
[Fact]
public void ParseTradeQuantity()
{
Assert.Throws<FormatException>(() => TradeQuantity.Parse("1.2345o"));
Assert.Throws<FormatException>(() => TradeQuantity.Parse("o"));
Assert.Throws<FormatException>(() => TradeQuantity.Parse(""));
Assert.Throws<FormatException>(() => TradeQuantity.Parse("1.353%%"));
Assert.Throws<FormatException>(() => TradeQuantity.Parse("1.353 %%"));
Assert.Throws<FormatException>(() => TradeQuantity.Parse("-1.353%"));
Assert.Throws<FormatException>(() => TradeQuantity.Parse("-1.353"));
var qty = TradeQuantity.Parse("1.3%");
Assert.Equal(1.3m, qty.Value);
Assert.Equal(TradeQuantity.ValueType.Percent, qty.Type);
var qty2 = TradeQuantity.Parse("1.3");
Assert.Equal(1.3m, qty2.Value);
Assert.Equal(TradeQuantity.ValueType.Exact, qty2.Type);
Assert.NotEqual(qty, qty2);
Assert.Equal(qty, TradeQuantity.Parse("1.3%"));
Assert.Equal(qty2, TradeQuantity.Parse("1.3"));
Assert.Equal(TradeQuantity.Parse(qty.ToString()), TradeQuantity.Parse("1.3%"));
Assert.Equal(TradeQuantity.Parse(qty2.ToString()), TradeQuantity.Parse("1.3"));
Assert.Equal(TradeQuantity.Parse(qty2.ToString()), TradeQuantity.Parse(" 1.3 "));
}
public static WalletFileParsers GetParsers()
{
var service = new ServiceCollection();
@ -2180,63 +2151,48 @@ bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku
var networkBTC = networkProvider.GetNetwork("BTC");
var networkLTC = networkProvider.GetNetwork("LTC");
InvoiceEntity invoiceEntity = new InvoiceEntity();
invoiceEntity.Networks = networkProvider;
invoiceEntity.Currency = "USD";
invoiceEntity.Payments = new System.Collections.Generic.List<PaymentEntity>();
invoiceEntity.Price = 100;
PaymentMethodDictionary paymentMethods = new PaymentMethodDictionary();
paymentMethods.Add(new PaymentMethod() { Network = networkBTC, Currency = "BTC", Rate = 10513.44m, }
.SetPaymentMethodDetails(
new BTCPayServer.Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod()
{
NextNetworkFee = Money.Coins(0.00000100m),
DepositAddress = dummy
}));
paymentMethods.Add(new PaymentMethod() { Network = networkLTC, Currency = "LTC", Rate = 216.79m }
.SetPaymentMethodDetails(
new BTCPayServer.Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod()
{
NextNetworkFee = Money.Coins(0.00010000m),
DepositAddress = dummy
}));
invoiceEntity.SetPaymentMethods(paymentMethods);
invoiceEntity.Rates.Add("BTC", 10513.44m);
invoiceEntity.Rates.Add("LTC", 216.79m);
PaymentPromptDictionary paymentMethods =
[
new () { PaymentMethodId = BTC, Divisibility = 8, Currency = "BTC", PaymentMethodFee = 0.00000100m, ParentEntity = invoiceEntity },
new () { PaymentMethodId = LTC, Divisibility = 8, Currency = "LTC", PaymentMethodFee = 0.00010000m, ParentEntity = invoiceEntity },
];
invoiceEntity.SetPaymentPrompts(paymentMethods);
var btc = invoiceEntity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike));
var btcId = PaymentTypes.CHAIN.GetPaymentMethodId("BTC");
var btc = invoiceEntity.GetPaymentPrompt(btcId);
var accounting = btc.Calculate();
invoiceEntity.Payments.Add(
new PaymentEntity()
{
Accounted = true,
Status = PaymentStatus.Settled,
Currency = "BTC",
NetworkFee = 0.00000100m,
Network = networkProvider.GetNetwork("BTC"),
}
.SetCryptoPaymentData(new BitcoinLikePaymentData()
{
Network = networkProvider.GetNetwork("BTC"),
Output = new TxOut() { Value = Money.Coins(0.00151263m) }
}));
PaymentMethodFee = 0.00000100m,
Value = 0.00151263m,
PaymentMethodId = btcId
});
invoiceEntity.UpdateTotals();
accounting = btc.Calculate();
invoiceEntity.Payments.Add(
new PaymentEntity()
{
Accounted = true,
Status = PaymentStatus.Settled,
Currency = "BTC",
NetworkFee = 0.00000100m,
Network = networkProvider.GetNetwork("BTC")
}
.SetCryptoPaymentData(new BitcoinLikePaymentData()
{
Network = networkProvider.GetNetwork("BTC"),
Output = new TxOut() { Value = Money.Coins(accounting.Due) }
}));
Value = accounting.Due,
PaymentMethodFee = 0.00000100m,
PaymentMethodId = btcId
});
invoiceEntity.UpdateTotals();
accounting = btc.Calculate();
Assert.Equal(0.0m, accounting.Due);
Assert.Equal(0.0m, accounting.DueUncapped);
var ltc = invoiceEntity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike));
var ltc = invoiceEntity.GetPaymentPrompt(PaymentTypes.CHAIN.GetPaymentMethodId("LTC"));
accounting = ltc.Calculate();
Assert.Equal(0.0m, accounting.Due);
@ -2284,42 +2240,172 @@ bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku
Assert.Null(metadata.PosData);
}
class CanOldMigrateInvoicesBlobVector
{
public string Type { get; set; }
public JObject Input { get; set; }
public JObject Expected { get; set; }
public bool SkipRountripTest { get; set; }
public Dictionary<string, string> ExpectedProperties { get; set; }
}
[Fact]
public void CanOldMigrateInvoicesBlob()
{
Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;
Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture;
int i = 0;
var vectors = JsonConvert.DeserializeObject<CanOldMigrateInvoicesBlobVector[]>(File.ReadAllText(TestUtils.GetTestDataFullPath("InvoiceMigrationTestVectors.json")));
foreach (var v in vectors)
{
TestLogs.LogInformation("Test " + i++);
object obj = null;
if (v.Type == "invoice")
{
Data.InvoiceData data = new Data.InvoiceData();
obj = data;
data.Blob2 = v.Input.ToString();
data.Migrate();
var actual = JObject.Parse(data.Blob2);
AssertSameJson(v.Expected, actual);
if (!v.SkipRountripTest)
{
// Check that we get the same as when setting blob again
var entity = data.GetBlob();
entity.AdditionalData?.Clear();
entity.SetPaymentPrompts(entity.GetPaymentPrompts()); // Cleanup
data.SetBlob(entity);
actual = JObject.Parse(data.Blob2);
AssertSameJson(v.Expected, actual);
}
}
else if (v.Type == "payment")
{
Data.PaymentData data = new Data.PaymentData();
//data.
obj = data;
data.Blob2 = v.Input.ToString();
data.Migrate();
var actual = JObject.Parse(data.Blob2);
AssertSameJson(v.Expected, actual);
if (!v.SkipRountripTest)
{
// Check that we get the same as when setting blob again
var entity = data.GetBlob();
data.SetBlob(entity);
actual = JObject.Parse(data.Blob2);
AssertSameJson(v.Expected, actual);
}
}
else
{
Assert.Fail("Unknown vector type");
}
if (v.ExpectedProperties is not null)
{
foreach (var kv in v.ExpectedProperties)
{
if (kv.Key == "CreatedInMs")
{
var actual = PaymentData.DateTimeToMilliUnixTime(((DateTimeOffset)obj.GetType().GetProperty("Created").GetValue(obj)).UtcDateTime);
Assert.Equal(long.Parse(kv.Value), actual);
}
else
{
var actual = obj.GetType().GetProperty(kv.Key).GetValue(obj);
Assert.Equal(kv.Value, actual?.ToString());
}
}
}
}
}
private void AssertSameJson(JToken expected, JToken actual, List<string> path = null)
{
var ok = JToken.DeepEquals(expected, actual);
if (ok)
return;
var e = NormalizeJsonString((JObject)expected);
var a = NormalizeJsonString((JObject)actual);
Assert.Equal(e, a);
}
public static string NormalizeJsonString(JObject parsedObject)
{
var normalizedObject = SortPropertiesAlphabetically(parsedObject);
return JsonConvert.SerializeObject(normalizedObject);
}
private static JObject SortPropertiesAlphabetically(JObject original)
{
var result = new JObject();
foreach (var property in original.Properties().ToList().OrderBy(p => p.Name))
{
var value = property.Value as JObject;
if (value != null)
{
value = SortPropertiesAlphabetically(value);
result.Add(property.Name, value);
}
else
{
result.Add(property.Name, property.Value);
}
}
return result;
}
[Fact]
public void CanParseInvoiceEntityDerivationStrategies()
{
var serializer = BlobSerializer.CreateSerializer(new NBXplorer.NBXplorerNetworkProvider(ChainName.Regtest).GetBTC()).Serializer;
// We have 3 ways of serializing the derivation strategies:
// through "derivationStrategy", through "derivationStrategies" as a string, through "derivationStrategies" as JObject
// Let's check that InvoiceEntity is similar in all cases.
var legacy = new JObject()
{
["derivationStrategy"] = "tpubDDLQZ1WMdy5YJAJWmRNoTJ3uQkavEPXCXnmD4eAuo9BKbzFUBbJmVHys5M3ku4Qw1C165wGpVWH55gZpHjdsCyntwNzhmCAzGejSL6rzbyf"
["derivationStrategy"] = "tpubDDLQZ1WMdy5YJAJWmRNoTJ3uQkavEPXCXnmD4eAuo9BKbzFUBbJmVHys5M3ku4Qw1C165wGpVWH55gZpHjdsCyntwNzhmCAzGejSL6rzbyf-[p2sh]"
};
var scheme = DerivationSchemeSettings.Parse("tpubDDLQZ1WMdy5YJAJWmRNoTJ3uQkavEPXCXnmD4eAuo9BKbzFUBbJmVHys5M3ku4Qw1C165wGpVWH55gZpHjdsCyntwNzhmCAzGejSL6rzbyf", CreateNetworkProvider(ChainName.Regtest).BTC);
Assert.True(scheme.AccountDerivation is DirectDerivationStrategy { Segwit: true });
var scheme = DerivationSchemeSettings.Parse("tpubDDLQZ1WMdy5YJAJWmRNoTJ3uQkavEPXCXnmD4eAuo9BKbzFUBbJmVHys5M3ku4Qw1C165wGpVWH55gZpHjdsCyntwNzhmCAzGejSL6rzbyf-[p2sh]", CreateNetworkProvider(ChainName.Regtest).BTC);
Assert.True(scheme.AccountDerivation is P2SHDerivationStrategy);
scheme.Source = "ManualDerivationScheme";
scheme.AccountOriginal = "tpubDDLQZ1WMdy5YJAJWmRNoTJ3uQkavEPXCXnmD4eAuo9BKbzFUBbJmVHys5M3ku4Qw1C165wGpVWH55gZpHjdsCyntwNzhmCAzGejSL6rzbyf";
scheme.AccountOriginal = "tpubDDLQZ1WMdy5YJAJWmRNoTJ3uQkavEPXCXnmD4eAuo9BKbzFUBbJmVHys5M3ku4Qw1C165wGpVWH55gZpHjdsCyntwNzhmCAzGejSL6rzbyf-[p2sh]";
var legacy2 = new JObject()
{
["derivationStrategies"] = scheme.ToJson()
["derivationStrategies"] = new JObject()
{
["BTC"] = JToken.FromObject(scheme, serializer)
}
};
var newformat = new JObject()
{
["derivationStrategies"] = JObject.Parse(scheme.ToJson())
["derivationStrategies"] = new JObject()
{
["BTC"] = JToken.FromObject(scheme, serializer)
}
};
//new BTCPayNetworkProvider(ChainName.Regtest)
#pragma warning disable CS0618 // Type or member is obsolete
var formats = new[] { legacy, legacy2, newformat }
.Select(o =>
{
var entity = JsonConvert.DeserializeObject<InvoiceEntity>(o.ToString());
entity.Networks = CreateNetworkProvider(ChainName.Regtest);
return entity.DerivationStrategies.ToString();
o.Add("currency", "USD");
o.Add("price", "0.0");
o.Add("cryptoData", new JObject()
{
["BTC"] = new JObject()
});
var data = new Data.InvoiceData();
data.Blob2 = o.ToString();
data.Migrate();
var migrated = JObject.Parse(data.Blob2);
return migrated["prompts"]["BTC-CHAIN"]["details"]["accountDerivation"].Value<string>();
})
.ToHashSet();
#pragma warning restore CS0618 // Type or member is obsolete
Assert.Single(formats);
var v = Assert.Single(formats);
Assert.NotNull(v);
}
[Fact]
@ -2328,25 +2414,8 @@ bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku
var pmi = "\"BTC_hasjdfhasjkfjlajn\"";
JsonTextReader reader = new(new StringReader(pmi));
reader.Read();
Assert.Null(new PaymentMethodIdJsonConverter().ReadJson(reader, typeof(PaymentMethodId), null,
JsonSerializer.CreateDefault()));
}
[Fact]
public void CanBeBracefulAfterObsoleteShitcoin()
{
var blob = new StoreBlob();
blob.PaymentMethodCriteria = new List<PaymentMethodCriteria>()
{
new()
{
Above = true,
Value = new CurrencyValue() {Currency = "BTC", Value = 0.1m},
PaymentMethod = new PaymentMethodId("BTC", PaymentTypes.BTCLike)
}
};
var newBlob = new Serializer(null).ToString(blob).Replace("paymentMethod\":\"BTC\"", "paymentMethod\":\"ETH_ZYC\"");
Assert.Empty(StoreDataExtensions.GetStoreBlob(new StoreData() { StoreBlob = newBlob }).PaymentMethodCriteria);
Assert.Equal("BTC-hasjdfhasjkfjlajn", new PaymentMethodIdJsonConverter().ReadJson(reader, typeof(PaymentMethodId), null,
JsonSerializer.CreateDefault()).ToString());
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,196 +0,0 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Custodians;
using BTCPayServer.Abstractions.Custodians.Client;
using BTCPayServer.Abstractions.Form;
using BTCPayServer.Client.Models;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Services.Custodian.Client.MockCustodian;
public class MockCustodian : ICustodian, ICanDeposit, ICanTrade, ICanWithdraw
{
public const string DepositPaymentMethod = "BTC-OnChain";
public const string DepositAddress = "bc1qxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
public const string TradeId = "TRADE-ID-001";
public const string TradeFromAsset = "EUR";
public const string TradeToAsset = "BTC";
public static readonly decimal TradeQtyBought = new decimal(1);
public static readonly decimal TradeFeeEuro = new decimal(12.5);
public static readonly decimal BtcPriceInEuro = new decimal(30000);
public const string WithdrawalPaymentMethod = "BTC-OnChain";
public const string WithdrawalAsset = "BTC";
public const string WithdrawalId = "WITHDRAWAL-ID-001";
public static readonly decimal WithdrawalAmount = new decimal(0.5);
public static readonly string WithdrawalAmountPercentage = "12.5%";
public static readonly decimal WithdrawalMinAmount = new decimal(0.001);
public static readonly decimal WithdrawalMaxAmount = new decimal(0.6);
public static readonly decimal WithdrawalFee = new decimal(0.0005);
public const string WithdrawalTransactionId = "yyy";
public const string WithdrawalTargetAddress = "bc1qyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy";
public const WithdrawalResponseData.WithdrawalStatus WithdrawalStatus = WithdrawalResponseData.WithdrawalStatus.Queued;
public static readonly decimal BalanceBTC = new decimal(1.23456);
public static readonly decimal BalanceLTC = new decimal(50.123456);
public static readonly decimal BalanceUSD = new decimal(1500.55);
public static readonly decimal BalanceEUR = new decimal(1235.15);
public string Code
{
get => "mock";
}
public string Name
{
get => "MOCK Exchange";
}
public Task<Dictionary<string, decimal>> GetAssetBalancesAsync(JObject config, CancellationToken cancellationToken)
{
var r = new Dictionary<string, decimal>()
{
{ "BTC", BalanceBTC }, { "LTC", BalanceLTC }, { "USD", BalanceUSD }, { "EUR", BalanceEUR },
};
return Task.FromResult(r);
}
public Task<Form> GetConfigForm(JObject config, CancellationToken cancellationToken = default)
{
return null;
}
public Task<DepositAddressData> GetDepositAddressAsync(string paymentMethod, JObject config, CancellationToken cancellationToken)
{
if (paymentMethod.Equals(DepositPaymentMethod))
{
var r = new DepositAddressData();
r.Address = DepositAddress;
return Task.FromResult(r);
}
throw new CustodianFeatureNotImplementedException($"Only BTC-OnChain is implemented for {this.Name}");
}
public string[] GetDepositablePaymentMethods()
{
return new[] { "BTC-OnChain" };
}
public List<AssetPairData> GetTradableAssetPairs()
{
var r = new List<AssetPairData>();
r.Add(new AssetPairData("BTC", "EUR", (decimal)0.0001));
return r;
}
private MarketTradeResult GetMarketTradeResult()
{
var ledgerEntries = new List<LedgerEntryData>();
ledgerEntries.Add(new LedgerEntryData("BTC", TradeQtyBought, LedgerEntryData.LedgerEntryType.Trade));
ledgerEntries.Add(new LedgerEntryData("EUR", -1 * TradeQtyBought * BtcPriceInEuro, LedgerEntryData.LedgerEntryType.Trade));
ledgerEntries.Add(new LedgerEntryData("EUR", -1 * TradeFeeEuro, LedgerEntryData.LedgerEntryType.Fee));
return new MarketTradeResult(TradeFromAsset, TradeToAsset, ledgerEntries, TradeId);
}
public Task<MarketTradeResult> TradeMarketAsync(string fromAsset, string toAsset, decimal qty, JObject config, CancellationToken cancellationToken)
{
if (!fromAsset.Equals("EUR") || !toAsset.Equals("BTC"))
{
throw new WrongTradingPairException(fromAsset, toAsset);
}
if (qty != TradeQtyBought)
{
throw new InsufficientFundsException($"With {Name}, you can only buy {TradeQtyBought} {TradeToAsset} with {TradeFromAsset} and nothing else.");
}
return Task.FromResult(GetMarketTradeResult());
}
public Task<MarketTradeResult> GetTradeInfoAsync(string tradeId, JObject config, CancellationToken cancellationToken)
{
if (tradeId == TradeId)
{
return Task.FromResult(GetMarketTradeResult());
}
return Task.FromResult<MarketTradeResult>(null);
}
public Task<AssetQuoteResult> GetQuoteForAssetAsync(string fromAsset, string toAsset, JObject config, CancellationToken cancellationToken)
{
if (fromAsset.Equals(TradeFromAsset) && toAsset.Equals(TradeToAsset))
{
return Task.FromResult(new AssetQuoteResult(TradeFromAsset, TradeToAsset, BtcPriceInEuro, BtcPriceInEuro));
}
throw new WrongTradingPairException(fromAsset, toAsset);
//throw new AssetQuoteUnavailableException(pair);
}
private WithdrawResult CreateWithdrawResult()
{
var ledgerEntries = new List<LedgerEntryData>();
ledgerEntries.Add(new LedgerEntryData(WithdrawalAsset, WithdrawalAmount - WithdrawalFee, LedgerEntryData.LedgerEntryType.Withdrawal));
ledgerEntries.Add(new LedgerEntryData(WithdrawalAsset, WithdrawalFee, LedgerEntryData.LedgerEntryType.Fee));
DateTimeOffset createdTime = new DateTimeOffset(2021, 9, 1, 6, 45, 0, new TimeSpan(-7, 0, 0));
var r = new WithdrawResult(WithdrawalPaymentMethod, WithdrawalAsset, ledgerEntries, WithdrawalId, WithdrawalStatus, createdTime, WithdrawalTargetAddress, WithdrawalTransactionId);
return r;
}
private SimulateWithdrawalResult CreateWithdrawSimulationResult()
{
var ledgerEntries = new List<LedgerEntryData>();
ledgerEntries.Add(new LedgerEntryData(WithdrawalAsset, WithdrawalAmount - WithdrawalFee, LedgerEntryData.LedgerEntryType.Withdrawal));
ledgerEntries.Add(new LedgerEntryData(WithdrawalAsset, WithdrawalFee, LedgerEntryData.LedgerEntryType.Fee));
var r = new SimulateWithdrawalResult(WithdrawalPaymentMethod, WithdrawalAsset, ledgerEntries, WithdrawalMinAmount, WithdrawalMaxAmount);
return r;
}
public Task<WithdrawResult> WithdrawToStoreWalletAsync(string paymentMethod, decimal amount, JObject config, CancellationToken cancellationToken)
{
if (paymentMethod == WithdrawalPaymentMethod)
{
if (amount.ToString(CultureInfo.InvariantCulture).Equals("" + WithdrawalAmount, StringComparison.InvariantCulture) || WithdrawalAmountPercentage.Equals(amount))
{
return Task.FromResult(CreateWithdrawResult());
}
throw new InsufficientFundsException($"{Name} only supports withdrawals of {WithdrawalAmount} or {WithdrawalAmountPercentage}");
}
throw new CannotWithdrawException(this, paymentMethod, $"Only {WithdrawalPaymentMethod} can be withdrawn from {Name}");
}
public Task<SimulateWithdrawalResult> SimulateWithdrawalAsync(string paymentMethod, decimal amount, JObject config, CancellationToken cancellationToken)
{
if (paymentMethod == WithdrawalPaymentMethod)
{
if (amount == WithdrawalAmount)
{
return Task.FromResult(CreateWithdrawSimulationResult());
}
throw new InsufficientFundsException($"{Name} only supports withdrawals of {WithdrawalAmount}");
}
throw new CannotWithdrawException(this, paymentMethod, $"Only {WithdrawalPaymentMethod} can be withdrawn from {Name}");
}
public Task<WithdrawResult> GetWithdrawalInfoAsync(string paymentMethod, string withdrawalId, JObject config, CancellationToken cancellationToken)
{
if (withdrawalId == WithdrawalId && WithdrawalPaymentMethod.Equals(paymentMethod))
{
return Task.FromResult(CreateWithdrawResult());
}
return Task.FromResult<WithdrawResult>(null);
}
public string[] GetWithdrawablePaymentMethods()
{
return GetDepositablePaymentMethods();
}
}

View File

@ -381,6 +381,7 @@ namespace BTCPayServer.Tests
return Task.CompletedTask;
});
s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success);
var handler = s.Server.PayTester.GetService<PaymentMethodHandlerDictionary>().GetBitcoinHandler("BTC");
await TestUtils.EventuallyAsync(async () =>
{
var invoice = await invoiceRepository.GetInvoice(invoiceId);
@ -389,19 +390,13 @@ namespace BTCPayServer.Tests
var originalPayment = payments[0];
var coinjoinPayment = payments[1];
Assert.Equal(-1,
((BitcoinLikePaymentData)originalPayment.GetCryptoPaymentData()).ConfirmationCount);
handler.ParsePaymentDetails(originalPayment.Details).ConfirmationCount);
Assert.Equal(0,
((BitcoinLikePaymentData)coinjoinPayment.GetCryptoPaymentData()).ConfirmationCount);
handler.ParsePaymentDetails(coinjoinPayment.Details).ConfirmationCount);
Assert.False(originalPayment.Accounted);
Assert.True(coinjoinPayment.Accounted);
Assert.Equal(((BitcoinLikePaymentData)originalPayment.GetCryptoPaymentData()).Value,
((BitcoinLikePaymentData)coinjoinPayment.GetCryptoPaymentData()).Value);
Assert.Equal(originalPayment.GetCryptoPaymentData()
.AssertType<BitcoinLikePaymentData>()
.Value,
coinjoinPayment.GetCryptoPaymentData()
.AssertType<BitcoinLikePaymentData>()
.Value);
Assert.Equal(originalPayment.Value,
coinjoinPayment.Value);
});
await TestUtils.EventuallyAsync(async () =>
@ -929,10 +924,9 @@ retry:
tester.ExplorerClient.Network.NBitcoinNetwork);
var senderStore = await tester.PayTester.StoreRepository.FindStore(senderUser.StoreId);
var paymentMethodId = new PaymentMethodId("BTC", PaymentTypes.BTCLike);
var derivationSchemeSettings = senderStore.GetSupportedPaymentMethods(tester.NetworkProvider)
.OfType<DerivationSchemeSettings>().SingleOrDefault(settings =>
settings.PaymentId == paymentMethodId);
var paymentMethodId = PaymentTypes.CHAIN.GetPaymentMethodId("BTC");
var handlers = tester.PayTester.GetService<PaymentMethodHandlerDictionary>();
var derivationSchemeSettings = senderStore.GetPaymentMethodConfig<DerivationSchemeSettings>(paymentMethodId, handlers);
ReceivedCoin[] senderCoins = null;
ReceivedCoin coin = null;
@ -1138,14 +1132,14 @@ retry:
//broadcast the payjoin
var res = (await tester.ExplorerClient.BroadcastAsync(Invoice7Coin6Response1TxSigned));
Assert.True(res.Success);
var handler = handlers.GetBitcoinHandler("BTC");
// Paid with coinjoin
await TestUtils.EventuallyAsync(async () =>
{
var invoiceEntity = await tester.PayTester.GetService<InvoiceRepository>().GetInvoice(invoice7.Id);
Assert.Equal(InvoiceStatusLegacy.Paid, invoiceEntity.Status);
Assert.Contains(invoiceEntity.GetPayments(false), p => p.Accounted &&
((BitcoinLikePaymentData)p.GetCryptoPaymentData()).PayjoinInformation is null);
handler.ParsePaymentDetails(p.Details).PayjoinInformation is null);
});
////Assert.Contains(receiverWalletPayJoinState.GetRecords(), item => item.InvoiceId == invoice7.Id && item.TxSeen);
@ -1174,7 +1168,7 @@ retry:
var invoiceEntity = await tester.PayTester.GetService<InvoiceRepository>().GetInvoice(invoice7.Id);
Assert.Equal(InvoiceStatusLegacy.New, invoiceEntity.Status);
Assert.True(invoiceEntity.GetPayments(false).All(p => !p.Accounted));
ourOutpoint = invoiceEntity.GetAllBitcoinPaymentData(false).First().PayjoinInformation.ContributedOutPoints[0];
ourOutpoint = invoiceEntity.GetAllBitcoinPaymentData(handler, false).First().PayjoinInformation.ContributedOutPoints[0];
});
var payjoinRepository = tester.PayTester.GetService<UTXOLocker>();
// The outpoint should now be available for next pj selection

View File

@ -27,6 +27,7 @@ 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;
@ -1550,17 +1551,16 @@ namespace BTCPayServer.Tests
{
await s.Server.ExplorerNode.SendToAddressAsync(address, Money.Coins(1.0m));
}
var handlers = s.Server.PayTester.GetService<PaymentMethodHandlerDictionary>();
var targetTx = await s.Server.ExplorerNode.SendToAddressAsync(address, Money.Coins(1.2m));
var tx = await s.Server.ExplorerNode.GetRawTransactionAsync(targetTx);
var spentOutpoint = new OutPoint(targetTx,
tx.Outputs.FindIndex(txout => txout.Value == Money.Coins(1.2m)));
var pmi = PaymentTypes.CHAIN.GetPaymentMethodId(walletId.CryptoCode);
await TestUtils.EventuallyAsync(async () =>
{
var store = await s.Server.PayTester.StoreRepository.FindStore(storeId);
var x = store.GetSupportedPaymentMethods(s.Server.NetworkProvider)
.OfType<DerivationSchemeSettings>()
.Single(settings => settings.PaymentId.CryptoCode == walletId.CryptoCode);
var x = store.GetPaymentMethodConfig<DerivationSchemeSettings>(pmi, handlers);
var wallet = s.Server.PayTester.GetService<BTCPayWalletProvider>().GetWallet(walletId.CryptoCode);
wallet.InvalidateCache(x.AccountDerivation);
Assert.Contains(
@ -1821,7 +1821,8 @@ namespace BTCPayServer.Tests
var invoiceId = s.CreateInvoice(storeId);
var invoice = await s.Server.PayTester.InvoiceRepository.GetInvoice(invoiceId);
var address = invoice.EntityToDTO().Addresses["BTC"];
var btc = PaymentTypes.CHAIN.GetPaymentMethodId("BTC");
var address = invoice.GetPaymentPrompt(btc).Destination;
//wallet should have been imported to bitcoin core wallet in watch only mode.
var result =
@ -1833,7 +1834,7 @@ namespace BTCPayServer.Tests
//lets import and save private keys
invoiceId = s.CreateInvoice(storeId);
invoice = await s.Server.PayTester.InvoiceRepository.GetInvoice(invoiceId);
address = invoice.EntityToDTO().Addresses["BTC"];
address = invoice.GetPaymentPrompt(btc).Destination;
result = await s.Server.ExplorerNode.GetAddressInfoAsync(
BitcoinAddress.Create(address, Network.RegTest));
//spendable from bitcoin core wallet!
@ -1895,8 +1896,7 @@ namespace BTCPayServer.Tests
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().CryptoInfo.First().PaymentUrls.BIP21;
var bip21 = invoice.EntityToDTO(s.Server.PayTester.GetService<Dictionary<PaymentMethodId, IPaymentMethodBitpayAPIExtension>>()).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);
@ -2257,7 +2257,7 @@ namespace BTCPayServer.Tests
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
s.GoToStore(newStore.storeId, StoreNavPages.Payouts);
s.Driver.FindElement(By.Id($"{new PaymentMethodId("BTC", PaymentTypes.LightningLike)}-view")).Click();
s.Driver.FindElement(By.Id($"{PaymentTypes.LN.GetPaymentMethodId("BTC")}-view")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-view")).Click();
s.Driver.FindElement(By.ClassName("mass-action-select-all")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-approve-pay")).Click();
@ -2267,7 +2267,7 @@ namespace BTCPayServer.Tests
s.FindAlertMessage();
s.GoToStore(newStore.storeId, StoreNavPages.Payouts);
s.Driver.FindElement(By.Id($"{new PaymentMethodId("BTC", PaymentTypes.LightningLike)}-view")).Click();
s.Driver.FindElement(By.Id($"{PaymentTypes.LN.GetPaymentMethodId("BTC")}-view")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.Completed}-view")).Click();
if (!s.Driver.PageSource.Contains(bolt))
@ -2277,7 +2277,7 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.ClassName("mass-action-select-all")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingPayment}-mark-paid")).Click();
s.Driver.FindElement(By.Id($"{new PaymentMethodId("BTC", PaymentTypes.LightningLike)}-view")).Click();
s.Driver.FindElement(By.Id($"{PaymentTypes.LN.GetPaymentMethodId("BTC")}-view")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.Completed}-view")).Click();
Assert.Contains(bolt, s.Driver.PageSource);
@ -2889,6 +2889,7 @@ namespace BTCPayServer.Tests
public async Task CanUseLNURL()
{
using var s = CreateSeleniumTester();
s.Server.DeleteStore = false;
s.Server.ActivateLightning();
await s.StartAsync();
await s.Server.EnsureChannelsSetup();
@ -3028,7 +3029,7 @@ namespace BTCPayServer.Tests
// Check that pull payment has lightning option
s.GoToStore(s.StoreId, StoreNavPages.PullPayments);
s.Driver.FindElement(By.Id("NewPullPayment")).Click();
Assert.Equal(new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike), PaymentMethodId.Parse(Assert.Single(s.Driver.FindElements(By.CssSelector("input[name='PaymentMethods']"))).GetAttribute("value")));
Assert.Equal(PaymentTypes.LN.GetPaymentMethodId(cryptoCode), PaymentMethodId.Parse(Assert.Single(s.Driver.FindElements(By.CssSelector("input[name='PaymentMethods']"))).GetAttribute("value")));
s.Driver.FindElement(By.Id("Name")).SendKeys("PP1");
s.Driver.FindElement(By.Id("Amount")).Clear();
s.Driver.FindElement(By.Id("Amount")).SendKeys("0.0000001");
@ -3053,7 +3054,7 @@ namespace BTCPayServer.Tests
s.GoToStore(s.StoreId, StoreNavPages.PullPayments);
var payouts = s.Driver.FindElements(By.ClassName("pp-payout"));
payouts[0].Click();
s.Driver.FindElement(By.Id("BTC_LightningLike-view")).Click();
s.Driver.FindElement(By.Id("BTC-LN-view")).Click();
Assert.NotEmpty(s.Driver.FindElements(By.ClassName("payout")));
s.Driver.FindElement(By.ClassName("mass-action-select-all")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-approve-pay")).Click();
@ -3164,14 +3165,14 @@ namespace BTCPayServer.Tests
Assert.Equal(2, invoices.Length);
foreach (var i in invoices)
{
var lightningPaymentMethod = i.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.LNURLPay));
var paymentMethodDetails =
lightningPaymentMethod.GetPaymentMethodDetails() as LNURLPayPaymentMethodDetails;
var prompt = i.GetPaymentPrompt(PaymentTypes.LNURL.GetPaymentMethodId("BTC"));
var handlers = s.Server.PayTester.GetService<PaymentMethodHandlerDictionary>();
var details = (LNURLPayPaymentMethodDetails)handlers.ParsePaymentPromptDetails(prompt);
Assert.Contains(
paymentMethodDetails.ConsumedLightningAddress,
details.ConsumedLightningAddress,
new[] { lnaddress1, lnaddress2 });
if (paymentMethodDetails.ConsumedLightningAddress == lnaddress2)
if (details.ConsumedLightningAddress == lnaddress2)
{
Assert.Equal("lol", i.Metadata.AdditionalData["test"].Value<string>());
}

View File

@ -112,7 +112,7 @@ namespace BTCPayServer.Tests
{
string connectionString = null;
if (connectionType is null)
return LightningSupportedPaymentMethod.InternalNode;
return LightningPaymentMethodConfig.InternalNode;
if (connectionType == LightningConnectionType.CLightning)
{
if (isMerchant)

View File

@ -5,6 +5,7 @@ using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Extensions;
@ -18,9 +19,11 @@ using BTCPayServer.Lightning;
using BTCPayServer.Lightning.CLightning;
using BTCPayServer.Models.AccountViewModels;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Payments.PayJoin.Sender;
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Wallets;
using BTCPayServer.Tests.Logging;
@ -29,6 +32,7 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.CodeAnalysis.Operations;
using Microsoft.EntityFrameworkCore;
using NBitcoin;
using NBitcoin.DataEncoders;
using NBitcoin.Payment;
using NBitpayClient;
using NBXplorer.DerivationStrategy;
@ -148,15 +152,6 @@ namespace BTCPayServer.Tests
await storeController.GeneralSettings(settings);
}
public async Task ModifyWalletSettings(Action<WalletSettingsViewModel> modify)
{
var storeController = GetController<UIStoresController>();
var response = await storeController.WalletSettings(StoreId, "BTC");
WalletSettingsViewModel walletSettings = (WalletSettingsViewModel)((ViewResult)response).Model;
modify(walletSettings);
storeController.UpdateWalletSettings(walletSettings).GetAwaiter().GetResult();
}
public async Task ModifyOnchainPaymentSettings(Action<WalletSettingsViewModel> modify)
{
var storeController = GetController<UIStoresController>();
@ -164,6 +159,7 @@ namespace BTCPayServer.Tests
WalletSettingsViewModel walletSettings = (WalletSettingsViewModel)((ViewResult)response).Model;
modify(walletSettings);
storeController.UpdatePaymentSettings(walletSettings).GetAwaiter().GetResult();
storeController.UpdateWalletSettings(walletSettings).GetAwaiter().GetResult();
}
public T GetController<T>(bool setImplicitStore = true) where T : Controller
@ -295,7 +291,7 @@ namespace BTCPayServer.Tests
var storeController = GetController<UIStoresController>();
var connectionString = parent.GetLightningConnectionString(connectionType, isMerchant);
var nodeType = connectionString == LightningSupportedPaymentMethod.InternalNode ? LightningNodeType.Internal : LightningNodeType.Custom;
var nodeType = connectionString == LightningPaymentMethodConfig.InternalNode ? LightningNodeType.Internal : LightningNodeType.Custom;
var vm = new LightningNodeViewModel { ConnectionString = connectionString, LightningNodeType = nodeType, SkipPortTest = true };
await storeController.SetupLightningNode(storeId ?? StoreId,
@ -373,8 +369,9 @@ namespace BTCPayServer.Tests
var pjClient = parent.PayTester.GetService<PayjoinClient>();
var storeRepository = parent.PayTester.GetService<StoreRepository>();
var store = await storeRepository.FindStore(StoreId);
var settings = store.GetSupportedPaymentMethods(parent.NetworkProvider).OfType<DerivationSchemeSettings>()
.First();
var pmi = PaymentTypes.CHAIN.GetPaymentMethodId(psbt.Network.NetworkSet.CryptoCode);
var handlers = parent.PayTester.GetService<PaymentMethodHandlerDictionary>();
var settings = store.GetPaymentMethodConfig<DerivationSchemeSettings>(pmi, handlers);
TestLogs.LogInformation($"Proposing {psbt.GetGlobalTransaction().GetHash()}");
if (expectedError is null && !senderError)
{
@ -491,7 +488,7 @@ namespace BTCPayServer.Tests
public class DummyStoreWebhookEvent : StoreWebhookEvent
{
}
public List<StoreWebhookEvent> WebhookEvents { get; set; } = new List<StoreWebhookEvent>();
@ -576,9 +573,10 @@ retry:
public async Task<uint256> PayOnChain(string invoiceId)
{
var cryptoCode = "BTC";
var pmi = PaymentTypes.CHAIN.GetPaymentMethodId(cryptoCode);
var client = await CreateClient();
var methods = await client.GetInvoicePaymentMethods(StoreId, invoiceId);
var method = methods.First(m => m.PaymentMethod == cryptoCode);
var method = methods.First(m => m.PaymentMethodId == pmi.ToString());
var address = method.Destination;
var tx = await client.CreateOnChainTransaction(StoreId, cryptoCode, new CreateOnChainTransactionRequest()
{
@ -601,7 +599,7 @@ retry:
var cryptoCode = "BTC";
var client = await CreateClient();
var methods = await client.GetInvoicePaymentMethods(StoreId, invoiceId);
var method = methods.First(m => m.PaymentMethod == $"{cryptoCode}-LightningNetwork");
var method = methods.First(m => m.PaymentMethodId == $"{cryptoCode}-LN");
var bolt11 = method.Destination;
TestLogs.LogInformation("PAYING");
await parent.CustomerLightningD.Pay(bolt11);
@ -615,7 +613,7 @@ retry:
var network = SupportedNetwork.NBitcoinNetwork;
var client = await CreateClient();
var methods = await client.GetInvoicePaymentMethods(StoreId, invoiceId);
var method = methods.First(m => m.PaymentMethod == $"{cryptoCode}-LNURLPAY");
var method = methods.First(m => m.PaymentMethodId == $"{cryptoCode}-LNURL");
var lnurL = LNURL.LNURL.Parse(method.PaymentLink, out var tag);
var http = new HttpClient();
var payreq = (LNURL.LNURLPayRequest)await LNURL.LNURL.FetchInformation(lnurL, tag, http);
@ -670,15 +668,36 @@ retry:
var dbContext = this.parent.PayTester.GetService<ApplicationDbContextFactory>().CreateContext();
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"))
{
foreach (var invoice in oldInvoices)
{
var localInvoice = invoice.Replace("3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd", storeId);
await writer.WriteLineAsync(localInvoice);
if (isHeader)
{
isHeader = false;
await writer.WriteLineAsync(invoice);
}
else
{
var localInvoice = invoice.Replace("3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd", storeId);
var fields = localInvoice.Split(',');
var blob1 = ZipUtils.Unzip(Encoders.Hex.DecodeData(fields[1].Substring(2)));
var matched = Regex.Match(blob1, "xpub[^\\\"-]*");
if (matched.Success)
{
var xpub = (BitcoinExtPubKey)Network.Main.Parse(matched.Value);
var xpubTestnet = xpub.ExtPubKey.GetWif(Network.RegTest).ToString();
blob1 = blob1.Replace(xpub.ToString(), xpubTestnet.ToString());
fields[1] = $"\\x{Encoders.Hex.EncodeData(ZipUtils.Zip(blob1))}";
localInvoice = string.Join(',', fields);
}
await writer.WriteLineAsync(localInvoice);
}
}
await writer.FlushAsync();
}
isHeader = true;
using (var writer = db.BeginTextImport("COPY \"Payments\" (\"Id\",\"Blob\",\"InvoiceDataId\",\"Accounted\",\"Blob2\",\"Type\") FROM STDIN DELIMITER ',' CSV HEADER"))
{
foreach (var invoice in oldPayments)

View File

@ -0,0 +1,731 @@
[
{
"type": "invoice",
"input": {
"id": "HuzCsv9hghew2FD6zVqUyY",
"storeId": "3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd",
"orderId": "CustomOrderId",
"speedPolicy": 0,
"rate": 5672.82929871779,
"invoiceTime": 1538395793,
"expirationTime": 1538396693,
"depositAddress": "39mWwvUDoZ5CxXa6CmgaUda19qYj9LpQD1",
"productInformation": {
"itemDesc": "Masternode Staking",
"itemCode": null,
"physical": false,
"price": 100.0,
"currency": "EUR",
"taxIncluded": 0.0
},
"buyerInformation": {
"buyerName": null,
"buyerEmail": "customer@example.com",
"buyerCountry": null,
"buyerZip": null,
"buyerState": null,
"buyerCity": null,
"buyerAddress2": null,
"buyerAddress1": null,
"buyerPhone": null
},
"posData": null,
"derivationStrategy": "xpub6DAbghfuRCW6Qqr3KfSDjik4kEUivAkfhcGHMkVEe4bZq53VQz79k44EoQ5MDpLJKKAPK5vVV6Px7U1R39eQibnbhDsW9uEQ1Kdh9CDbmgR-[p2sh]",
"derivationStrategies": "{\"BTC\":\"xpub6DAbghfuRCW6Qqr3KfSDjik4kEUivAkfhcGHMkVEe4bZq53VQz79k44EoQ5MDpLJKKAPK5vVV6Px7U1R39eQibnbhDsW9uEQ1Kdh9CDbmgR-[p2sh]\"}",
"status": "new",
"exceptionStatus": null,
"payments": [],
"refundable": false,
"refundMail": "customer@example.com",
"redirectURL": "https://example.com/thanksyou",
"txFee": 100,
"fullNotifications": true,
"notificationURL": "https://example.com/callbacks",
"serverUrl": "https://mainnet.demo.btcpayserver.org",
"cryptoData": {
"BTC": {
"rate": 5672.82929871779,
"paymentMethod": {},
"feeRate": 1,
"txFee": 100,
"depositAddress": "39mWwvUDoZ5CxXa6CmgaUda19qYj9LpQD1"
}
},
"monitoringExpiration": 1538400293,
"historicalAddresses": null,
"availableAddressHashes": null,
"extendedNotifications": false,
"events": null,
"paymentTolerance": 1.0
},
"expected": {
"version": 3,
"metadata": {
"orderId": "CustomOrderId",
"itemDesc": "Masternode Staking",
"physical": false,
"buyerEmail": "customer@example.com",
"taxIncluded": 0.0
},
"rates": { "BTC": "5672.82929871779" },
"serverUrl": "https://mainnet.demo.btcpayserver.org",
"prompts": {
"BTC-CHAIN": {
"currency": "BTC",
"paymentMethodFee": "0.000001",
"details": {
"paymentMethodFeeRate": 1,
"recommendedFeeRate": 1,
"accountDerivation": "xpub6DAbghfuRCW6Qqr3KfSDjik4kEUivAkfhcGHMkVEe4bZq53VQz79k44EoQ5MDpLJKKAPK5vVV6Px7U1R39eQibnbhDsW9uEQ1Kdh9CDbmgR-[p2sh]"
},
"divisibility": 8,
"destination": "39mWwvUDoZ5CxXa6CmgaUda19qYj9LpQD1"
}
},
"redirectURL": "https://example.com/thanksyou",
"receiptOptions": {},
"internalTags": [],
"expirationTime": 1538396693,
"notificationURL": "https://example.com/callbacks",
"paymentTolerance": 1.0,
"fullNotifications": true,
"monitoringExpiration": 1538400293
}
},
{
"type": "invoice",
"input": {
"id": "ANjq7kkV4mqDRPr5F8EmLZ",
"rate": "66823.066",
"price": "30",
"txFee": 0,
"events": null,
"status": "new",
"refunds": null,
"storeId": "8Ja5pfPydLZTt3YLWhZuZ5vfXT2bNfvk1krUhF8ykCyt",
"version": 2,
"customerEmail": "toto@toto.com",
"archived": false,
"currency": "USD",
"metadata": {
"orderId": "CC",
"itemDesc": "CC"
},
"payments": [],
"serverUrl": "https://mainnet.demo.btcpayserver.org",
"cryptoData": {
"BTC": {
"rate": 66823.066,
"txFee": 0,
"feeRate": 15.13,
"paymentMethod": {
"keyPath": "0/25903",
"activated": true,
"networkFeeRate": 12.319,
"payjoinEnabled": false
},
"depositAddress": "bc1quf9l42tnma7zws9vusrvun4wchau0ue4zj838g"
}
},
"paidAmount": {
"net": 0.0,
"gross": 0.0,
"currency": "USD"
},
"refundMail": null,
"invoiceTime": 1709806449,
"isUnderPaid": true,
"redirectURL": "https://test/",
"speedPolicy": 1,
"checkoutType": null,
"internalTags": [],
"depositAddress": "bc1quf9l42tnma7zws9vusrvun4wchau0ue4zj838g",
"expirationTime": 1709808249,
"receiptOptions": {
"showQR": null,
"enabled": null,
"showPayments": null
},
"defaultLanguage": null,
"exceptionStatus": "",
"notificationURL": null,
"storeSupportUrl": null,
"paymentTolerance": 0.0,
"fullNotifications": true,
"notificationEmail": null,
"lazyPaymentMethods": false,
"requiresRefundEmail": null,
"defaultPaymentMethod": "BTC",
"derivationStrategies": {
"BTC": {
"label": null,
"source": "ManualDerivationScheme",
"signingKey": "xpub6DAbghfuRCW6Qqr3KfSDjik4kEUivAkfhcGHMkVEe4bZq53VQz79k44EoQ5MDpLJKKAPK5vVV6Px7U1R39eQibnbhDsW9uEQ1Kdh9CDbmgR-[p2sh]",
"isHotWallet": false,
"accountOriginal": null,
"accountDerivation": "xpub6DAbghfuRCW6Qqr3KfSDjik4kEUivAkfhcGHMkVEe4bZq53VQz79k44EoQ5MDpLJKKAPK5vVV6Px7U1R39eQibnbhDsW9uEQ1Kdh9CDbmgR-[p2sh]",
"accountKeySettings": [
{
"accountKey": "xpub6DAbghfuRCW6Qqr3KfSDjik4kEUivAkfhcGHMkVEe4bZq53VQz79k44EoQ5MDpLJKKAPK5vVV6Px7U1R39eQibnbhDsW9uEQ1Kdh9CDbmgR-[p2sh]",
"accountKeyPath": "84/0'/0'",
"rootFingerprint": "312e13db"
}
]
}
},
"monitoringExpiration": 1709894649,
"extendedNotifications": false,
"redirectAutomatically": false,
"availableAddressHashes": null
},
"expected": {
"version": 3,
"metadata": {
"orderId": "CC",
"itemDesc": "CC",
"buyerEmail": "toto@toto.com"
},
"serverUrl": "https://mainnet.demo.btcpayserver.org",
"rates": { "BTC": "66823.066" },
"prompts": {
"BTC-CHAIN": {
"currency": "BTC",
"destination": "bc1quf9l42tnma7zws9vusrvun4wchau0ue4zj838g",
"details": {
"recommendedFeeRate": 15.13,
"paymentMethodFeeRate": 12.319,
"keyPath": "0/25903",
"accountDerivation": "xpub6DAbghfuRCW6Qqr3KfSDjik4kEUivAkfhcGHMkVEe4bZq53VQz79k44EoQ5MDpLJKKAPK5vVV6Px7U1R39eQibnbhDsW9uEQ1Kdh9CDbmgR-[p2sh]"
},
"divisibility": 8
}
},
"receiptOptions": {},
"redirectURL": "https://test/",
"speedPolicy": 1,
"internalTags": [],
"expirationTime": 1709808249,
"fullNotifications": true,
"defaultPaymentMethod": "BTC-CHAIN",
"monitoringExpiration": 1709894649
}
},
{
"type": "invoice",
"input": {
"currency": "USD",
"price": 0.0,
"cryptoData": {
"BTC": {
"feeRate": 10.0,
"paymentMethod": {
},
"depositAddress": "bc1q9l42tnma7zws9vusrvun4wchau0ue4zj838g"
}
}
},
"expected": {
"internalTags": [],
"metadata": {},
"version": 3,
"prompts": {
"BTC-CHAIN": {
"currency": "BTC",
"details": {
"paymentMethodFeeRate": 10.0,
"recommendedFeeRate": 10.0
},
"divisibility": 8,
"destination": "bc1q9l42tnma7zws9vusrvun4wchau0ue4zj838g"
}
},
"receiptOptions": {}
},
"skipRountripTest" : true
},
{
"type": "invoice",
"input": {
"currency": "USD",
"price": 0.0,
"cryptoData": {
"BTC": {
"feeRate": 4.0,
"paymentMethod": {
"networkFeeMode": 2
},
"depositAddress": "bc1q9l42tnma7zws9vusrvun4wchau0ue4zj838g"
}
}
},
"expected": {
"internalTags": [],
"metadata": {},
"version": 3,
"prompts": {
"BTC-CHAIN": {
"currency": "BTC",
"details": {
"feeMode": "Never",
"recommendedFeeRate": 4.0
},
"divisibility": 8,
"destination": "bc1q9l42tnma7zws9vusrvun4wchau0ue4zj838g"
}
},
"receiptOptions": {}
},
"skipRountripTest": true
},
{
"type": "invoice",
"input": {
"id": "AmoigMwzbaBNNCfo21yns1",
"rate": "67056.018",
"price": "13",
"txFee": null,
"events": null,
"status": "new",
"refunds": null,
"storeId": "2b4H99crZ4JuPiRQwUuYpLQsjtHWASLVWHHkQQ2moiED",
"version": 2,
"archived": false,
"currency": "USD",
"metadata": {},
"payments": [],
"serverUrl": "https://mainnet.demo.btcpayserver.org",
"cryptoData": {
"BTC": {
"rate": 67056.018,
"txFee": null,
"feeRate": null,
"paymentMethod": {
"keyPath": null,
"activated": false,
"networkFeeMode": 0,
"networkFeeRate": null,
"payjoinEnabled": false
},
"depositAddress": null
},
"BTC_LNURLPAY": {
"rate": 67056.018,
"txFee": null,
"feeRate": null,
"paymentMethod": {
"BOLT11": null,
"NodeInfo": "03d2a44997a0fb6deee0a31c389d9d6bcb6f929f1dd0ba67201d195f2b3c76087c@170.75.160.16:9735",
"Preimage": {},
"Activated": true,
"InvoiceId": null,
"Bech32Mode": true,
"PayRequest": null,
"PaymentHash": {},
"ProvidedComment": null,
"GeneratedBoltAmount": null,
"ConsumedLightningAddress": null,
"LightningSupportedPaymentMethod": {
"CryptoCode": "BTC",
"InternalNodeRef": "Internal Node"
}
},
"depositAddress": null
},
"BTC_LightningLike": {
"rate": 67056.018,
"txFee": null,
"feeRate": null,
"paymentMethod": {
"BOLT11": null,
"NodeInfo": null,
"Preimage": null,
"Activated": false,
"InvoiceId": null,
"PaymentHash": null
},
"depositAddress": null
}
},
"paidAmount": {
"net": 0.0,
"gross": 0.0,
"currency": "USD"
},
"refundMail": null,
"invoiceTime": 1709864059,
"isUnderPaid": true,
"redirectURL": null,
"speedPolicy": 1,
"checkoutType": null,
"internalTags": [],
"depositAddress": null,
"expirationTime": 1709864959,
"receiptOptions": {
"showQR": null,
"enabled": null,
"showPayments": null
},
"defaultLanguage": null,
"exceptionStatus": "",
"notificationURL": null,
"storeSupportUrl": null,
"paymentTolerance": 0.0,
"fullNotifications": true,
"notificationEmail": null,
"lazyPaymentMethods": true,
"requiresRefundEmail": false,
"defaultPaymentMethod": "BTC",
"derivationStrategies": {
"BTC": {
"label": null,
"source": "NBXplorerGenerated",
"signingKey": "xpub6DAbghfuRCW6Qqr3KfSDjik4kEUivAkfhcGHMkVEe4bZq53VQz79k44EoQ5MDpLJKKAPK5vVV6Px7U1R39eQibnbhDsW9uEQ1Kdh9CDbmgR",
"isHotWallet": true,
"accountOriginal": "xpub6DAbghfuRCW6Qqr3KfSDjik4kEUivAkfhcGHMkVEe4bZq53VQz79k44EoQ5MDpLJKKAPK5vVV6Px7U1R39eQibnbhDsW9uEQ1Kdh9CDbmgR",
"accountDerivation": "xpub6DAbghfuRCW6Qqr3KfSDjik4kEUivAkfhcGHMkVEe4bZq53VQz79k44EoQ5MDpLJKKAPK5vVV6Px7U1R39eQibnbhDsW9uEQ1Kdh9CDbmgR",
"accountKeySettings": [
{
"accountKey": "xpub6DAbghfuRCW6Qqr3KfSDjik4kEUivAkfhcGHMkVEe4bZq53VQz79k44EoQ5MDpLJKKAPK5vVV6Px7U1R39eQibnbhDsW9uEQ1Kdh9CDbmgR",
"accountKeyPath": "84'/0'/0'",
"rootFingerprint": "312e13db"
}
]
},
"BTC_LNURLPAY": {
"CryptoCode": "BTC",
"LUD12Enabled": false,
"UseBech32Scheme": true
},
"BTC_LightningLike": {
"CryptoCode": "BTC",
"InternalNodeRef": "Internal Node"
}
},
"monitoringExpiration": 1709951359,
"extendedNotifications": false,
"redirectAutomatically": false,
"availableAddressHashes": null
},
"expected": {
"version": 3,
"metadata": {},
"serverUrl": "https://mainnet.demo.btcpayserver.org",
"rates": { "BTC": "67056.018" },
"prompts": {
"BTC-CHAIN": {
"inactive": true,
"currency": "BTC",
"details": {
"accountDerivation": "xpub6DAbghfuRCW6Qqr3KfSDjik4kEUivAkfhcGHMkVEe4bZq53VQz79k44EoQ5MDpLJKKAPK5vVV6Px7U1R39eQibnbhDsW9uEQ1Kdh9CDbmgR"
},
"divisibility": 8
},
"BTC-LNURL": {
"currency": "BTC",
"divisibility": 11,
"details": {
"nodeInfo": "03d2a44997a0fb6deee0a31c389d9d6bcb6f929f1dd0ba67201d195f2b3c76087c@170.75.160.16:9735",
"bech32Mode": true
}
},
"BTC-LN": {
"currency": "BTC",
"divisibility": 11,
"inactive": true,
"details": {}
}
},
"speedPolicy": 1,
"expirationTime": 1709864959,
"receiptOptions": {},
"fullNotifications": true,
"lazyPaymentMethods": true,
"defaultPaymentMethod": "BTC-CHAIN",
"internalTags": [],
"monitoringExpiration": 1709951359
}
},
{
"type": "invoice",
"input": {
"id": "ULzMvaSEpvV4XxGb6F78LZ",
"rate": "0",
"type": "TopUp",
"price": "0.0",
"txFee": null,
"events": null,
"status": "new",
"refunds": null,
"storeId": "EBeAyDVUwBSNa6bdZNrytay9ARNL5UB9xnvNPh5xdnhy",
"version": 2,
"archived": false,
"currency": "USD",
"metadata": {
},
"payments": [
],
"serverUrl": "https://donate.nicolas-dorier.com",
"cryptoData": {
"BTC_LNURLPAY": {
"rate": 29501.4,
"txFee": null,
"feeRate": null,
"paymentMethod": {
"BOLT11": "lnbc10n1pjn9nmzpp5a7znt4tv9gy5v6342xrgnntltkljffp255dph40vaf6964j27pvqhp59ly2g7flsy97vqahh9yue8qz7u6tvlpjfh9r0m9nzfezhm6fgmqscqzzsxqzursp55l9zht4zya3jyjdr9khr22z6afvjqdcw06l7vyd6tksdtsc8ezqs9qyyssqwswp9dnz9txv8t8zjrrts9rv4agu40ufqc04434f6lszdwvlhjk45m3pdcpqzghswkrcvgeaztcr6h82xp35suu64hnk4ms929pcahgpfg7sza",
"NodeInfo": "03d2a44997a0fb6deee0a31c389d9d6bcb6f929f1dd0ba67201d195f2b3c76087c@170.75.160.16:9735",
"Preimage": null,
"Activated": true,
"InvoiceId": "ef8535d56c2a09466a35518689cd7f5dbf24a42aa51a1bd5ecea745d564af058",
"Bech32Mode": true,
"PayRequest": {
"tag": "payRequest",
"callback": "https://donate.nicolas-dorier.com/BTC/UILNURL/pay/i/ULzMvaSEpvV4XxGb6F78LZ",
"metadata": "[[\"text/identifier\",\"donate@donate.nicolas-dorier.com\"],[\"text/plain\",\"Paid to Nicolas Donation Store (Order ID: )\"]]",
"maxSendable": 612000000000,
"minSendable": 1000,
"commentAllowed": 0
},
"PaymentHash": "ef8535d56c2a09466a35518689cd7f5dbf24a42aa51a1bd5ecea745d564af058",
"ProvidedComment": null,
"GeneratedBoltAmount": "1000",
"ConsumedLightningAddress": "donate@donate.nicolas-dorier.com",
"LightningSupportedPaymentMethod": {
"CryptoCode": "BTC",
"InternalNodeRef": "Internal Node"
}
},
"depositAddress": null
}
},
"paidAmount": {
"net": 0.0,
"gross": 0.0,
"currency": "USD"
},
"refundMail": null,
"invoiceTime": 1697828706,
"isUnderPaid": false,
"redirectURL": null,
"speedPolicy": 1,
"checkoutType": null,
"internalTags": [
],
"depositAddress": null,
"expirationTime": 1697829606,
"receiptOptions": {
"showQR": null,
"enabled": null,
"showPayments": null
},
"defaultLanguage": null,
"exceptionStatus": "",
"notificationURL": null,
"storeSupportUrl": null,
"paymentTolerance": 0.0,
"fullNotifications": false,
"notificationEmail": null,
"lazyPaymentMethods": false,
"requiresRefundEmail": null,
"defaultPaymentMethod": "BTC",
"derivationStrategies": {
"BTC_LNURLPAY": {
"CryptoCode": "BTC",
"LUD12Enabled": false,
"UseBech32Scheme": true
}
},
"monitoringExpiration": 1697833206,
"extendedNotifications": false,
"redirectAutomatically": false,
"availableAddressHashes": null
},
"expected": {
"type": "TopUp",
"version": 3,
"rates": {
"BTC": "29501.4"
},
"metadata": {
},
"serverUrl": "https://donate.nicolas-dorier.com",
"prompts": {
"BTC-LNURL": {
"currency": "BTC",
"divisibility": 11,
"destination": "lnbc10n1pjn9nmzpp5a7znt4tv9gy5v6342xrgnntltkljffp255dph40vaf6964j27pvqhp59ly2g7flsy97vqahh9yue8qz7u6tvlpjfh9r0m9nzfezhm6fgmqscqzzsxqzursp55l9zht4zya3jyjdr9khr22z6afvjqdcw06l7vyd6tksdtsc8ezqs9qyyssqwswp9dnz9txv8t8zjrrts9rv4agu40ufqc04434f6lszdwvlhjk45m3pdcpqzghswkrcvgeaztcr6h82xp35suu64hnk4ms929pcahgpfg7sza",
"details": {
"nodeInfo": "03d2a44997a0fb6deee0a31c389d9d6bcb6f929f1dd0ba67201d195f2b3c76087c@170.75.160.16:9735",
"invoiceId": "ef8535d56c2a09466a35518689cd7f5dbf24a42aa51a1bd5ecea745d564af058",
"bech32Mode": true,
"payRequest": {
"tag": "payRequest",
"callback": "https://donate.nicolas-dorier.com/BTC/UILNURL/pay/i/ULzMvaSEpvV4XxGb6F78LZ",
"metadata": "[[\"text/identifier\",\"donate@donate.nicolas-dorier.com\"],[\"text/plain\",\"Paid to Nicolas Donation Store (Order ID: )\"]]",
"maxSendable": 612000000000,
"minSendable": 1000,
"commentAllowed": 0
},
"paymentHash": "ef8535d56c2a09466a35518689cd7f5dbf24a42aa51a1bd5ecea745d564af058",
"generatedBoltAmount": "1000",
"consumedLightningAddress": "donate@donate.nicolas-dorier.com"
}
}
},
"speedPolicy": 1,
"internalTags": [],
"expirationTime": 1697829606,
"receiptOptions": {},
"defaultPaymentMethod": "BTC-CHAIN",
"monitoringExpiration": 1697833206
}
},
{
"type": "payment",
"input": {
"version": 1,
"receivedTime": 1556044076,
"networkFee": 0.000002,
"outpoint": "552b74ff2fc2c8564de40c8cbefd8eb78f1bef5d3010009c643ff889e159663a3d000000",
"output": "7a636f000000000017a914fd2a2d1d15eb6a490512953c12d6db0de662741d87",
"accounted": true,
"cryptoCode": "BTC",
"cryptoPaymentData": "{\"ConfirmationCount\":1414,\"RBF\":false,\"NetworkFee\":0.0,\"Legacy\":false}",
"cryptoPaymentDataType": "BTCLike"
},
"expected": {
"version": 2,
"destination": "3QmdQXq3tSMqiuwNy2ZcbFEHxZ5De4SBYJ",
"paymentMethodFee": "0.000002",
"divisibility": 8,
"details": {
"confirmationCount": 1414,
"outpoint": "3a6659e189f83f649c0010305def1b8fb78efdbe8c0ce44d56c8c22fff742b55-61"
}
},
"expectedProperties": {
"Created": "04/23/2019 18:27:56 +00:00",
"Type": "BTC-CHAIN",
"Currency": "BTC",
"Status": "Settled",
"Amount": "0.07299962",
"Accounted": null
}
},
{
"type": "payment",
"input": {
"receivedTime": 1538403202,
"outpoint": "3211391d9dd2d01c8d9f164d0231f72a166c9b27b15fb2603f58a193ca47bea800000000",
"output": "c74500000000000017a9145d741911858531a4e0a8b6a58bf5036d7f68857587",
"accounted": true,
"cryptoCode": "BTC",
"cryptoPaymentData": "{\"ConfirmationCount\":19,\"RBF\":true,\"Legacy\":false}",
"cryptoPaymentDataType": "BTCLike"
},
"expected": {
"destination": "3AD9r1UyXXNFA2o3cucBwqX58xNaXvrdsv",
"divisibility": 8,
"details": {
"confirmationCount": 19,
"rbf": true,
"outpoint": "a8be47ca93a1583f60b25fb1279b6c162af731024d169f8d1cd0d29d1d391132-0"
},
"version": 2
},
"expectedProperties": {
"Created": "10/01/2018 14:13:22 +00:00",
"Type": "BTC-CHAIN",
"Currency": "BTC",
"Status": "Settled",
"Amount": "0.00017863",
"Accounted": null
}
},
{
"type": "payment",
"input": {
"output": null,
"version": 1,
"outpoint": null,
"accounted": true,
"cryptoCode": "BTC",
"networkFee": 0.0,
"receivedTimeMs": 1711005875969,
"cryptoPaymentData": "{\"amount\":1000,\"bolT11\":\"lnbc10n1pjlhc90pp5smwdey0c5skr758lpm0w7tf56cafmdd0hey2nylwfl9q398jgf5shp5qra0y2q5w98at2vv0upux3sn2p0efrxs4h2nyzghqcj7009nqpsqcqzzsxqyz5vqsp5y8fvj3dxnwdaavx89nc5vykuywcmzdefh7x7z62q873hmawh7pws9qyyssq8f7ptu037y2vcclwst6nj8cy8ndhrcxj729ea4ntwdmpgtrwfqun5jnh4zul9s7vvtqd58wurdzk9e3xpzs3ykm0umjdwcttfussaxqqayadfs\",\"paymentHash\":\"86dcdc91f8a42c3f50ff0edeef2d34d63a9db5afbe48a993ee4fca0894f24269\",\"paymentType\":\"LNURLPAY\",\"networkFee\":0.0}",
"cryptoPaymentDataType": "LNURLPAY"
},
"expected": {
"version": 2,
"divisibility": 11,
"destination": "lnbc10n1pjlhc90pp5smwdey0c5skr758lpm0w7tf56cafmdd0hey2nylwfl9q398jgf5shp5qra0y2q5w98at2vv0upux3sn2p0efrxs4h2nyzghqcj7009nqpsqcqzzsxqyz5vqsp5y8fvj3dxnwdaavx89nc5vykuywcmzdefh7x7z62q873hmawh7pws9qyyssq8f7ptu037y2vcclwst6nj8cy8ndhrcxj729ea4ntwdmpgtrwfqun5jnh4zul9s7vvtqd58wurdzk9e3xpzs3ykm0umjdwcttfussaxqqayadfs",
"details": {
"paymentHash": "86dcdc91f8a42c3f50ff0edeef2d34d63a9db5afbe48a993ee4fca0894f24269"
}
},
"expectedProperties": {
"Created": "03/21/2024 07:24:35 +00:00",
"CreatedInMs": "1711005875969",
"Amount": "0.00000001",
"Type": "BTC-LNURL",
"Currency": "BTC"
}
},
{
"type": "payment",
"input": {
"output": null,
"version": 1,
"outpoint": "2e0ee2cccec304926677621ab5c1c80723f695740f1aed6915b4e72995d8172016000000",
"accounted": true,
"cryptoCode": "BTC",
"networkFee": 0.0,
"receivedTimeMs": 1710974348741,
"cryptoPaymentData": "{\"confirmationCount\":6,\"rbf\":false,\"address\":\"bc1qdamnd0fjegj4a5efrwx4gvjc69zufmu7ntf5ft\",\"keyPath\":\"0/708\",\"value\":197864,\"legacy\":false}",
"cryptoPaymentDataType": "BTCLike"
},
"expected": {
"destination": "bc1qdamnd0fjegj4a5efrwx4gvjc69zufmu7ntf5ft",
"divisibility": 8,
"details": {
"confirmationCount": 6,
"keyPath": "0/708",
"outpoint": "2017d89529e7b41569ed1a0f7495f62307c8c1b51a6277669204c3cecce20e2e-22"
},
"version": 2
},
"expectedProperties": {
"Created": "03/20/2024 22:39:08 +00:00",
"CreatedInMs": "1710974348741",
"Amount": "0.00197864",
"Type": "BTC-CHAIN",
"Currency": "BTC",
"Status": "Settled",
"Accounted": null
}
},
{
"type": "payment",
"input": {
"output": null,
"version": 1,
"outpoint": "2e0ee2cccec304926677621ab5c1c80723f695740f1aed6915b4e72995d8172016000000",
"accounted": true,
"cryptoCode": "BTC",
"networkFee": 0.0,
"receivedTimeMs": 1710974348741,
"cryptoPaymentData": "{\"confirmationCount\":6,\"rbf\":true,\"address\":\"bc1qdamnd0fjegj4a5efrwx4gvjc69zufmu7ntf5ft\",\"keyPath\":\"0/708\",\"value\":197864,\"legacy\":false}",
"cryptoPaymentDataType": "BTCLike"
},
"expected": {
"divisibility": 8,
"destination": "bc1qdamnd0fjegj4a5efrwx4gvjc69zufmu7ntf5ft",
"details": {
"confirmationCount": 6,
"keyPath": "0/708",
"outpoint": "2017d89529e7b41569ed1a0f7495f62307c8c1b51a6277669204c3cecce20e2e-22",
"RBF": true
},
"version": 2
}
}
]

View File

@ -423,9 +423,11 @@ retry:
[Trait("Fast", "Fast")]
public async Task CheckJsContent()
{
var handler = new HttpClientHandler();
handler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate | DecompressionMethods.Brotli;
// This test verify that no malicious js is added in the minified files.
// We should extend the tests to other js files, but we can do as we go...
using var client = new HttpClient();
using var client = new HttpClient(handler);
var actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "bootstrap", "bootstrap.bundle.min.js").Trim();
var version = Regex.Match(actual, "Bootstrap v([0-9]+.[0-9]+.[0-9]+)").Groups[1].Value;
var expected = (await (await client.GetAsync($"https://cdn.jsdelivr.net/npm/bootstrap@{version}/dist/js/bootstrap.bundle.min.js")).Content.ReadAsStringAsync()).Trim();

View File

@ -40,6 +40,7 @@ using BTCPayServer.Payments.PayJoin.Sender;
using BTCPayServer.Plugins.PayButton;
using BTCPayServer.Plugins.PointOfSale;
using BTCPayServer.Plugins.PointOfSale.Controllers;
using BTCPayServer.Rating;
using BTCPayServer.Security.Bitpay;
using BTCPayServer.Security.Greenfield;
using BTCPayServer.Services;
@ -69,6 +70,7 @@ using NBXplorer.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Schema;
using Npgsql;
using Xunit;
using Xunit.Abstractions;
using Xunit.Sdk;
@ -377,14 +379,16 @@ namespace BTCPayServer.Tests
await user.RegisterDerivationSchemeAsync("BTC");
await user.RegisterLightningNodeAsync("BTC", LightningConnectionType.CLightning);
await user.SetNetworkFeeMode(NetworkFeeMode.Never);
await user.ModifyWalletSettings(p => p.SpeedPolicy = SpeedPolicy.HighSpeed);
await user.ModifyOnchainPaymentSettings(p => p.SpeedPolicy = SpeedPolicy.HighSpeed);
var invoice = await user.BitPay.CreateInvoiceAsync(new Invoice(0.0001m, "BTC"));
await tester.WaitForEvent<InvoiceNewPaymentDetailsEvent>(async () =>
{
await tester.ExplorerNode.SendToAddressAsync(
BitcoinAddress.Create(invoice.BitcoinAddress, Network.RegTest), Money.Coins(0.00005m));
}, e => e.InvoiceId == invoice.Id && e.PaymentMethodId.PaymentType == LightningPaymentType.Instance);
await tester.ExplorerNode.GenerateAsync(1);
BitcoinAddress.Create(invoice.BitcoinAddress, Network.RegTest), Money.Coins(0.00005m), new NBitcoin.RPC.SendToAddressParameters()
{
Replaceable = false
});
}, e => e.InvoiceId == invoice.Id && e.PaymentMethodId == PaymentTypes.LN.GetPaymentMethodId("BTC"));
Invoice newInvoice = null;
await TestUtils.EventuallyAsync(async () =>
{
@ -560,7 +564,7 @@ namespace BTCPayServer.Tests
var acc = tester.NewAccount();
acc.GrantAccess();
acc.RegisterDerivationScheme("BTC");
await acc.ModifyWalletSettings(p => p.SpeedPolicy = SpeedPolicy.LowSpeed);
await acc.ModifyOnchainPaymentSettings(p => p.SpeedPolicy = SpeedPolicy.LowSpeed);
var invoice = acc.BitPay.CreateInvoice(new Invoice
{
Price = 5.0m,
@ -1043,16 +1047,17 @@ namespace BTCPayServer.Tests
tx1.ToString(),
}).Result["txid"].Value<string>());
TestLogs.LogInformation($"Bumped with {tx1Bump}");
var handler = tester.PayTester.GetService<PaymentMethodHandlerDictionary>().GetBitcoinHandler("BTC");
await TestUtils.EventuallyAsync(async () =>
{
var invoiceEntity = await tester.PayTester.InvoiceRepository.GetInvoice(invoice.Id);
var btcPayments = invoiceEntity.GetAllBitcoinPaymentData(false).ToArray();
var btcPayments = invoiceEntity.GetAllBitcoinPaymentData(handler, false).ToArray();
var payments = invoiceEntity.GetPayments(false).ToArray();
Assert.Equal(tx1, btcPayments[0].Outpoint.Hash);
Assert.False(payments[0].Accounted);
Assert.Equal(tx1Bump, payments[1].Outpoint.Hash);
Assert.Equal(tx1Bump, btcPayments[1].Outpoint.Hash);
Assert.True(payments[1].Accounted);
Assert.Equal(0.0m, payments[1].NetworkFee);
Assert.Equal(0.0m, payments[1].PaymentMethodFee);
invoice = user.BitPay.GetInvoice(invoice.Id);
Assert.Equal(payment1, invoice.BtcPaid);
Assert.Equal("paid", invoice.Status);
@ -1085,8 +1090,8 @@ namespace BTCPayServer.Tests
Assert.IsType<ViewResult>(await user.GetController<UIInvoiceController>().Invoice(invoice.Id)).Model)
.Payments;
Assert.Single(payments);
var paymentData = payments.First().GetCryptoPaymentData() as BitcoinLikePaymentData;
Assert.NotNull(paymentData.KeyPath);
var paymentData = payments.First().Details;
Assert.NotNull(paymentData["keyPath"]);
}
[Fact(Timeout = LongRunningTestTimeout)]
@ -1339,12 +1344,10 @@ namespace BTCPayServer.Tests
var btcmethod = (await client.GetInvoicePaymentMethods(user.StoreId, invoice.Id))[0];
var paid = btcSent;
var invoiceAddress = BitcoinAddress.Create(btcmethod.Destination, cashCow.Network);
var btc = new PaymentMethodId("BTC", PaymentTypes.BTCLike);
var btc = PaymentTypes.CHAIN.GetPaymentMethodId("BTC");
var networkFee = (await tester.PayTester.InvoiceRepository.GetInvoice(invoice.Id))
.GetPaymentMethods()[btc]
.GetPaymentMethodDetails()
.AssertType<BitcoinLikeOnChainPaymentMethod>()
.GetNextNetworkFee();
.GetPaymentPrompt(btc)
.PaymentMethodFee;
if (networkFeeMode != NetworkFeeMode.Always)
{
networkFee = 0.0m;
@ -1364,7 +1367,7 @@ namespace BTCPayServer.Tests
Assert.Equal("False", bitpayinvoice.ExceptionStatus.ToString());
// Check if we index by price correctly once we know it
var invoices = await client.GetInvoices(user.StoreId, textSearch: $"{bitpayinvoice.Price.ToString(CultureInfo.InvariantCulture)}");
var invoices = await client.GetInvoices(user.StoreId, textSearch: bitpayinvoice.Price.ToString(CultureInfo.InvariantCulture).Split('.')[0]);
Assert.Contains(invoices, inv => inv.Id == bitpayinvoice.Id);
}
catch (JsonSerializationException)
@ -1492,15 +1495,15 @@ namespace BTCPayServer.Tests
await user.RegisterLightningNodeAsync("BTC");
var lnMethod = new PaymentMethodId("BTC", PaymentTypes.LightningLike).ToString();
var btcMethod = new PaymentMethodId("BTC", PaymentTypes.BTCLike).ToString();
var lnMethod = PaymentTypes.LN.GetPaymentMethodId("BTC").ToString();
var btcMethod = PaymentTypes.CHAIN.GetPaymentMethodId("BTC").ToString();
// We allow BTC and LN, but not BTC under 5 USD, so only LN should be in the invoice
var vm = Assert.IsType<CheckoutAppearanceViewModel>(Assert
.IsType<ViewResult>(user.GetController<UIStoresController>().CheckoutAppearance()).Model);
Assert.Equal(2, vm.PaymentMethodCriteria.Count);
var criteria = Assert.Single(vm.PaymentMethodCriteria.Where(m => m.PaymentMethod == btcMethod.ToString()));
Assert.Equal(new PaymentMethodId("BTC", BitcoinPaymentType.Instance).ToString(), criteria.PaymentMethod);
Assert.Equal(PaymentTypes.CHAIN.GetPaymentMethodId("BTC").ToString(), criteria.PaymentMethod);
criteria.Value = "5 USD";
criteria.Type = PaymentMethodCriteriaViewModel.CriteriaType.GreaterThan;
Assert.IsType<RedirectToActionResult>(user.GetController<UIStoresController>().CheckoutAppearance(vm)
@ -1518,8 +1521,8 @@ namespace BTCPayServer.Tests
}, Facade.Merchant);
// LN and LNURL
Assert.Equal(2, invoice.CryptoInfo.Length);
Assert.Contains(invoice.CryptoInfo, c => c.PaymentType == PaymentTypes.LNURLPay.ToString());
Assert.Contains(invoice.CryptoInfo, c => c.PaymentType == PaymentTypes.LightningLike.ToString());
Assert.Contains(invoice.CryptoInfo, c => c.PaymentType == "BTC-LNURL");
Assert.Contains(invoice.CryptoInfo, c => c.PaymentType == "BTC-LN");
// Let's replicate https://github.com/btcpayserver/btcpayserver/issues/2963
// We allow BTC for more than 5 USD, and LN for less than 150. The default is LN, so the default
@ -1639,7 +1642,7 @@ namespace BTCPayServer.Tests
user.SetLNUrl(cryptoCode, false);
var vm = user.GetController<UIStoresController>().CheckoutAppearance().AssertViewModel<CheckoutAppearanceViewModel>();
var criteria = Assert.Single(vm.PaymentMethodCriteria);
Assert.Equal(new PaymentMethodId(cryptoCode, LightningPaymentType.Instance).ToString(), criteria.PaymentMethod);
Assert.Equal(PaymentTypes.LN.GetPaymentMethodId(cryptoCode).ToString(), criteria.PaymentMethod);
criteria.Value = "2 USD";
criteria.Type = PaymentMethodCriteriaViewModel.CriteriaType.LessThan;
Assert.IsType<RedirectToActionResult>(user.GetController<UIStoresController>().CheckoutAppearance(vm)
@ -1652,14 +1655,14 @@ namespace BTCPayServer.Tests
Currency = "USD"
}, Facade.Merchant);
Assert.Single(invoice.CryptoInfo);
Assert.Equal(PaymentTypes.LightningLike.ToString(), invoice.CryptoInfo[0].PaymentType);
Assert.Equal("BTC-LN", invoice.CryptoInfo[0].PaymentType);
// Activating LNUrl, we should still have only 1 payment criteria that can be set.
user.RegisterLightningNode(cryptoCode);
user.SetLNUrl(cryptoCode, true);
vm = user.GetController<UIStoresController>().CheckoutAppearance().AssertViewModel<CheckoutAppearanceViewModel>();
criteria = Assert.Single(vm.PaymentMethodCriteria);
Assert.Equal(new PaymentMethodId(cryptoCode, LightningPaymentType.Instance).ToString(), criteria.PaymentMethod);
Assert.Equal(PaymentTypes.LN.GetPaymentMethodId(cryptoCode).ToString(), criteria.PaymentMethod);
Assert.IsType<RedirectToActionResult>(user.GetController<UIStoresController>().CheckoutAppearance(vm).Result);
// However, creating an invoice should show LNURL
@ -1713,7 +1716,7 @@ namespace BTCPayServer.Tests
public async Task CanChangeNetworkFeeMode()
{
using var tester = CreateServerTester();
var btc = new PaymentMethodId("BTC", PaymentTypes.BTCLike);
var btc = PaymentTypes.CHAIN.GetPaymentMethodId("BTC");
await tester.StartAsync();
var user = tester.NewAccount();
user.GrantAccess();
@ -1733,10 +1736,7 @@ namespace BTCPayServer.Tests
FullNotifications = true
}, Facade.Merchant);
var nextNetworkFee = (await tester.PayTester.InvoiceRepository.GetInvoice(invoice.Id))
.GetPaymentMethods()[btc]
.GetPaymentMethodDetails()
.AssertType<BitcoinLikeOnChainPaymentMethod>()
.GetNextNetworkFee();
.GetPaymentPrompt(btc).PaymentMethodFee;
var firstPaymentFee = nextNetworkFee;
switch (networkFeeMode)
{
@ -1768,10 +1768,8 @@ namespace BTCPayServer.Tests
TestLogs.LogInformation($"Remaining due after first payment: {due}");
Assert.Equal(Money.Coins(firstPayment), Money.Parse(invoice.CryptoInfo[0].Paid));
nextNetworkFee = (await tester.PayTester.InvoiceRepository.GetInvoice(invoice.Id))
.GetPaymentMethods()[btc]
.GetPaymentMethodDetails()
.AssertType<BitcoinLikeOnChainPaymentMethod>()
.GetNextNetworkFee();
.GetPaymentPrompt(btc)
.PaymentMethodFee;
switch (networkFeeMode)
{
case NetworkFeeMode.Never:
@ -1942,10 +1940,10 @@ namespace BTCPayServer.Tests
var repo = tester.PayTester.GetService<InvoiceRepository>();
var entity = (await repo.GetInvoice(invoice6.Id));
Assert.Equal((decimal)ulong.MaxValue, entity.Price);
entity.GetPaymentMethods().First().Calculate();
entity.GetPaymentPrompts().First().Calculate();
// Shouldn't be possible as we clamp the value, but existing invoice may have that
entity.Price = decimal.MaxValue;
entity.GetPaymentMethods().First().Calculate();
entity.GetPaymentPrompts().First().Calculate();
}
@ -1979,14 +1977,14 @@ namespace BTCPayServer.Tests
});
var invoicePaymentRequest = new BitcoinUrlBuilder((await client.GetInvoicePaymentMethods(user.StoreId, invoice.Id)).Single(model =>
PaymentMethodId.Parse(model.PaymentMethod) ==
new PaymentMethodId("BTC", BitcoinPaymentType.Instance))
PaymentMethodId.Parse(model.PaymentMethodId) ==
PaymentTypes.CHAIN.GetPaymentMethodId("BTC"))
.PaymentLink, tester.ExplorerNode.Network);
var halfPaymentTx = await tester.ExplorerNode.SendToAddressAsync(invoicePaymentRequest.Address, Money.Coins(invoicePaymentRequest.Amount.ToDecimal(MoneyUnit.BTC)/2m));
invoicePaymentRequest = new BitcoinUrlBuilder((await client.GetInvoicePaymentMethods(user.StoreId, invoice.Id)).Single(model =>
PaymentMethodId.Parse(model.PaymentMethod) ==
new PaymentMethodId("BTC", BitcoinPaymentType.Instance))
PaymentMethodId.Parse(model.PaymentMethodId) ==
PaymentTypes.CHAIN.GetPaymentMethodId("BTC"))
.PaymentLink, tester.ExplorerNode.Network);
var remainingPaymentTx = await tester.ExplorerNode.SendToAddressAsync(invoicePaymentRequest.Address, Money.Coins(invoicePaymentRequest.Amount.ToDecimal(MoneyUnit.BTC)));
@ -2028,8 +2026,8 @@ namespace BTCPayServer.Tests
Currency = "BTC",
});
invoicePaymentRequest = new BitcoinUrlBuilder((await client.GetInvoicePaymentMethods(user.StoreId, invoice.Id)).Single(model =>
PaymentMethodId.Parse(model.PaymentMethod) ==
new PaymentMethodId("BTC", BitcoinPaymentType.Instance))
PaymentMethodId.Parse(model.PaymentMethodId) ==
PaymentTypes.CHAIN.GetPaymentMethodId("BTC"))
.PaymentLink, tester.ExplorerNode.Network);
halfPaymentTx = await tester.ExplorerNode.SendToAddressAsync(invoicePaymentRequest.Address, Money.Coins(invoicePaymentRequest.Amount.ToDecimal(MoneyUnit.BTC)/2m));
@ -2140,7 +2138,7 @@ namespace BTCPayServer.Tests
var ctx = tester.PayTester.GetService<ApplicationDbContextFactory>().CreateContext();
Assert.Equal(0, invoice.CryptoInfo[0].TxCount);
Assert.True(invoice.MinerFees.ContainsKey("BTC"));
Assert.Contains(invoice.MinerFees["BTC"].SatoshiPerBytes, new[] { 100.0m, 20.0m });
Assert.Contains(Math.Round(invoice.MinerFees["BTC"].SatoshiPerBytes), new[] { 100.0m, 20.0m });
TestUtils.Eventually(() =>
{
var textSearchResult = tester.PayTester.InvoiceRepository.GetInvoices(new InvoiceQuery()
@ -2218,14 +2216,6 @@ namespace BTCPayServer.Tests
await cashCow.GenerateAsync(1); //The user has medium speed settings, so 1 conf is enough to be confirmed
TestUtils.Eventually(() =>
{
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("confirmed", localInvoice.Status);
});
await cashCow.GenerateAsync(5); //Now should be complete
TestUtils.Eventually(() =>
{
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
@ -2268,7 +2258,7 @@ namespace BTCPayServer.Tests
TestUtils.Eventually(() =>
{
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("confirmed", localInvoice.Status);
Assert.Equal("complete", localInvoice.Status);
Assert.Equal(Money.Zero, localInvoice.BtcDue);
Assert.Equal("paidOver", (string)((JValue)localInvoice.ExceptionStatus).Value);
});
@ -2289,7 +2279,7 @@ namespace BTCPayServer.Tests
c =>
{
Assert.False(c.AfterExpiration);
Assert.Equal(new PaymentMethodId("BTC", PaymentTypes.BTCLike).ToStringNormalized(), c.PaymentMethod);
Assert.Equal(PaymentTypes.CHAIN.GetPaymentMethodId("BTC").ToString(), c.PaymentMethodId);
Assert.NotNull(c.Payment);
Assert.Equal(invoice.BitcoinAddress, c.Payment.Destination);
Assert.StartsWith(txId.ToString(), c.Payment.Id);
@ -2299,7 +2289,7 @@ namespace BTCPayServer.Tests
c =>
{
Assert.False(c.AfterExpiration);
Assert.Equal(new PaymentMethodId("BTC", PaymentTypes.BTCLike).ToStringNormalized(), c.PaymentMethod);
Assert.Equal(PaymentTypes.CHAIN.GetPaymentMethodId("BTC").ToString(), c.PaymentMethodId);
Assert.NotNull(c.Payment);
Assert.Equal(invoice.BitcoinAddress, c.Payment.Destination);
Assert.StartsWith(txId.ToString(), c.Payment.Id);
@ -2539,7 +2529,9 @@ namespace BTCPayServer.Tests
await RestartMigration(tester);
store = await tester.PayTester.StoreRepository.FindStore(acc.StoreId);
Assert.True(string.IsNullOrEmpty(store.DerivationStrategy));
var v = (DerivationSchemeSettings)store.GetSupportedPaymentMethods(tester.NetworkProvider).First();
var handlers = tester.PayTester.GetService<PaymentMethodHandlerDictionary>();
var pmi = PaymentTypes.CHAIN.GetPaymentMethodId("BTC");
var v = store.GetPaymentMethodConfig<DerivationSchemeSettings>(pmi, handlers);
Assert.Equal(derivation, v.AccountDerivation.ToString());
Assert.Equal(derivation, v.AccountOriginal.ToString());
Assert.Equal(xpub, v.SigningKey.ToString());
@ -2547,13 +2539,26 @@ namespace BTCPayServer.Tests
await acc.RegisterLightningNodeAsync("BTC", LightningConnectionType.CLightning, true);
store = await tester.PayTester.StoreRepository.FindStore(acc.StoreId);
var lnMethod = store.GetSupportedPaymentMethods(tester.NetworkProvider).OfType<LightningSupportedPaymentMethod>().First();
pmi = PaymentTypes.LN.GetPaymentMethodId("BTC");
var lnMethod = store.GetPaymentMethodConfig<LightningPaymentMethodConfig>(pmi, handlers);
Assert.NotNull(lnMethod.GetExternalLightningUrl());
var conf = store.GetPaymentMethodConfig(pmi);
conf["LightningConnectionString"] = conf["connectionString"].Value<string>();
conf["DisableBOLT11PaymentOption"] = true;
((JObject)conf).Remove("connectionString");
store.SetPaymentMethodConfig(pmi, conf);
await tester.PayTester.StoreRepository.UpdateStore(store);
await RestartMigration(tester);
store = await tester.PayTester.StoreRepository.FindStore(acc.StoreId);
lnMethod = store.GetSupportedPaymentMethods(tester.NetworkProvider).OfType<LightningSupportedPaymentMethod>().First();
lnMethod = store.GetPaymentMethodConfig<LightningPaymentMethodConfig>(pmi, handlers);
Assert.Null(lnMethod.GetExternalLightningUrl());
Assert.True(lnMethod.IsInternalNode);
conf = store.GetPaymentMethodConfig(pmi);
Assert.Null(conf["CryptoCode"]); // Osolete
Assert.Null(conf["connectionString"]); // Null, so should be stripped
Assert.Null(conf["DisableBOLT11PaymentOption"]); // Old garbage cleaned
// Test if legacy lightning charge settings are converted to LightningConnectionString
store.DerivationStrategies = new JObject()
@ -2569,9 +2574,8 @@ namespace BTCPayServer.Tests
}.ToString();
await tester.PayTester.StoreRepository.UpdateStore(store);
await RestartMigration(tester);
store = await tester.PayTester.StoreRepository.FindStore(acc.StoreId);
lnMethod = store.GetSupportedPaymentMethods(tester.NetworkProvider).OfType<LightningSupportedPaymentMethod>().First();
lnMethod = store.GetPaymentMethodConfig<LightningPaymentMethodConfig>(pmi, handlers);
Assert.NotNull(lnMethod.GetExternalLightningUrl());
var url = lnMethod.GetExternalLightningUrl();
@ -2596,8 +2600,23 @@ namespace BTCPayServer.Tests
await tester.PayTester.StoreRepository.UpdateStore(store);
await RestartMigration(tester);
store = await tester.PayTester.StoreRepository.FindStore(acc.StoreId);
lnMethod = store.GetSupportedPaymentMethods(tester.NetworkProvider).OfType<LightningSupportedPaymentMethod>().First();
lnMethod = store.GetPaymentMethodConfig<LightningPaymentMethodConfig>(pmi, handlers);
Assert.True(lnMethod.IsInternalNode);
store.SetPaymentMethodConfig(PaymentMethodId.Parse("BTC-LNURL"),
new JObject()
{
["CryptoCode"] = "BTC",
["LUD12Enabled"] = true,
["UseBech32Scheme"] = false,
});
await tester.PayTester.StoreRepository.UpdateStore(store);
await RestartMigration(tester);
store = await tester.PayTester.StoreRepository.FindStore(acc.StoreId);
conf = store.GetPaymentMethodConfig(PaymentMethodId.Parse("BTC-LNURL"));
Assert.Null(conf["CryptoCode"]);
Assert.True(conf["lud12Enabled"].Value<bool>());
Assert.Null(conf["useBech32Scheme"]); // default stripped
}
[Fact(Timeout = LongRunningTestTimeout)]
@ -2725,7 +2744,7 @@ namespace BTCPayServer.Tests
serializer.ToString(new Dictionary<string, string>()
{
{
new PaymentMethodId("BTC", BitcoinPaymentType.Instance).ToString(),
PaymentTypes.CHAIN.GetPaymentMethodId("BTC").ToString(),
new KeyPath("44'/0'/0'").ToString()
}
})));
@ -2756,13 +2775,33 @@ namespace BTCPayServer.Tests
Assert.Empty(blob.AdditionalData);
Assert.Single(blob.PaymentMethodCriteria);
Assert.Contains(blob.PaymentMethodCriteria,
criteria => criteria.PaymentMethod == new PaymentMethodId("BTC", BitcoinPaymentType.Instance) &&
criteria => criteria.PaymentMethod == PaymentTypes.CHAIN.GetPaymentMethodId("BTC") &&
criteria.Above && criteria.Value.Value == 5m && criteria.Value.Currency == "USD");
Assert.Equal(NetworkFeeMode.Never, blob.NetworkFeeMode);
Assert.Contains(store.GetSupportedPaymentMethods(tester.NetworkProvider), method =>
method is DerivationSchemeSettings dss &&
method.PaymentId == new PaymentMethodId("BTC", BitcoinPaymentType.Instance) &&
dss.AccountKeyPath == new KeyPath("44'/0'/0'"));
var handlers = tester.PayTester.GetService<PaymentMethodHandlerDictionary>();
Assert.Contains(store.GetPaymentMethodConfigs(handlers), method =>
method.Value is DerivationSchemeSettings dss &&
method.Key == PaymentTypes.CHAIN.GetPaymentMethodId("BTC") &&
dss.AccountKeySettings[0].AccountKeyPath == new KeyPath("44'/0'/0'"));
await acc.ImportOldInvoices();
var dbContext = tester.PayTester.GetService<ApplicationDbContextFactory>().CreateContext();
var invoiceMigrator = tester.PayTester.GetService<InvoiceBlobMigratorHostedService>();
invoiceMigrator.BatchSize = 2;
await invoiceMigrator.ResetMigration();
await invoiceMigrator.StartAsync(default);
tester.DeleteStore = false;
await TestUtils.EventuallyAsync(async () =>
{
var invoices = await dbContext.Invoices.AsNoTracking().ToListAsync();
foreach (var invoice in invoices)
{
Assert.NotNull(invoice.Currency);
Assert.NotNull(invoice.Amount);
Assert.NotNull(invoice.Blob2);
}
Assert.True(await invoiceMigrator.IsComplete());
});
}
private static async Task RestartMigration(ServerTester tester)
@ -3062,7 +3101,7 @@ namespace BTCPayServer.Tests
// 1 payment on chain
Assert.Equal(4, report.Data.Count);
var lnAddressIndex = report.GetIndex("LightningAddress");
var paymentTypeIndex = report.GetIndex("PaymentType");
var paymentTypeIndex = report.GetIndex("Category");
Assert.Contains(report.Data, d => d[lnAddressIndex]?.Value<string>()?.Contains(acc.LNAddress) is true);
var paymentTypes = report.Data
.GroupBy(d => d[paymentTypeIndex].Value<string>())

View File

@ -216,5 +216,5 @@
</Content>
</ItemGroup>
<ProjectExtensions><VisualStudio><UserProperties wwwroot_4swagger_4v1_4swagger_1template_1invoices_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1misc_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1pull-payments_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1serverinfo_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1stores_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1users_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1webhooks_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" /></VisualStudio></ProjectExtensions>
<ProjectExtensions><VisualStudio><UserProperties wwwroot_4swagger_4v1_4swagger_1template_1invoices_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1misc_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1pull-payments_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1serverinfo_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1stores_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1stores-payment-methods_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1users_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1webhooks_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" /></VisualStudio></ProjectExtensions>
</Project>

View File

@ -1,7 +1,8 @@
@using BTCPayServer.Payments
@using BTCPayServer.Services.Invoices
@using BTCPayServer.Abstractions.Extensions
@model BTCPayServer.Components.InvoiceStatus.InvoiceStatusViewModel
@inject PaymentMethodHandlerDictionary PaymentMethodHandlerDictionary
@inject Dictionary<PaymentMethodId, IPaymentModelExtension> Extensions
@{
var state = Model.State.ToString();
@ -41,16 +42,17 @@
</div>
@if (Model.Payments != null)
{
foreach (var paymentMethodId in Model.Payments.Select(payment => payment.GetPaymentMethodId()).Distinct())
foreach (var paymentMethodId in Model.Payments.Select(payment => payment.PaymentMethodId).Distinct())
{
var image = PaymentMethodHandlerDictionary[paymentMethodId]?.GetCryptoImage(paymentMethodId);
var badge = paymentMethodId.PaymentType.GetBadge();
var extension = Extensions.TryGetValue(paymentMethodId, out var e) ? e : null;
var image = extension?.Image;
var badge = extension?.Badge;
if (!string.IsNullOrEmpty(image) || !string.IsNullOrEmpty(badge))
{
<span class="d-inline-flex align-items-center gap-1">
@if (!string.IsNullOrEmpty(image))
{
<img src="@Context.Request.GetRelativePathOrAbsolute(image)" alt="@paymentMethodId.PaymentType.ToString()" style="height:1.5em" />
<img src="@Context.Request.GetRelativePathOrAbsolute(image)" alt="@paymentMethodId.ToString()" style="height:1.5em" />
}
@if (!string.IsNullOrEmpty(badge))
{

View File

@ -10,13 +10,13 @@
@using BTCPayServer.Plugins
@using BTCPayServer.Services
@using BTCPayServer.Views.Apps
@using BTCPayServer.Views.CustodianAccounts
@inject Microsoft.AspNetCore.Http.IHttpContextAccessor HttpContext;
@inject BTCPayServerEnvironment Env
@inject SignInManager<ApplicationUser> SignInManager
@inject PoliciesSettings PoliciesSettings
@inject ThemeSettings Theme
@inject PluginService PluginService
@inject PrettyNameProvider PrettyName
@model BTCPayServer.Components.MainNav.MainNavViewModel
@ -61,14 +61,14 @@
{
<a asp-area="" asp-controller="UIWallets" asp-action="WalletTransactions" asp-route-walletId="@scheme.WalletId" class="nav-link @ViewData.IsActiveCategory(typeof(WalletsNavPages), scheme.WalletId.ToString()) @ViewData.IsActivePage(StoreNavPages.OnchainSettings)" id="@($"StoreNav-Wallet{scheme.Crypto}")">
<span class="me-2 btcpay-status btcpay-status--@(scheme.Enabled ? "enabled" : "pending")"></span>
<span>@(Model.AltcoinsBuild ? $"{scheme.Crypto} Wallet" : "Bitcoin")</span>
<span>@PrettyName.PrettyName(scheme.PaymentMethodId)</span>
</a>
}
else
{
<a asp-area="" asp-controller="UIStores" asp-action="SetupWallet" asp-route-cryptoCode="@scheme.Crypto" asp-route-storeId="@Model.Store.Id" class="nav-link @ViewData.IsActivePage(StoreNavPages.OnchainSettings)" id="@($"StoreNav-Wallet{scheme.Crypto}")">
<span class="me-2 btcpay-status btcpay-status--@(scheme.Enabled ? "enabled" : "pending")"></span>
<span>@(Model.AltcoinsBuild ? $"{scheme.Crypto} Wallet" : "Bitcoin")</span>
<span>@PrettyName.PrettyName(scheme.PaymentMethodId)</span>
</a>
}
</li>
@ -81,40 +81,20 @@
{
<a asp-area="" asp-controller="UIStores" asp-action="Lightning" asp-route-cryptoCode="@scheme.CryptoCode" asp-route-storeId="@Model.Store.Id" class="nav-link @ViewData.IsActivePage(StoreNavPages.Lightning) @ViewData.IsActivePage(StoreNavPages.LightningSettings)" id="@($"StoreNav-Lightning{scheme.CryptoCode}")">
<span class="me-2 btcpay-status btcpay-status--@(scheme.Enabled ? "enabled" : "pending")"></span>
<span>@(Model.AltcoinsBuild ? $"{scheme.CryptoCode} " : "")Lightning</span>
<span>@PrettyName.PrettyName(scheme.PaymentMethodId)</span>
</a>
}
else
{
<a asp-area="" asp-controller="UIStores" asp-action="SetupLightningNode" asp-route-cryptoCode="@scheme.CryptoCode" asp-route-storeId="@Model.Store.Id" class="nav-link @ViewData.IsActivePage(StoreNavPages.LightningSettings)" id="@($"StoreNav-Lightning{scheme.CryptoCode}")">
<span class="me-2 btcpay-status btcpay-status--@(scheme.Enabled ? "enabled" : "pending")"></span>
<span>@(Model.AltcoinsBuild ? $"{scheme.CryptoCode} " : "")Lightning</span>
<span>@PrettyName.PrettyName(scheme.PaymentMethodId)</span>
</a>
}
</li>
}
<vc:ui-extension-point location="store-wallets-nav" model="@Model"/>
@if (PoliciesSettings.Experimental)
{
@foreach (var custodianAccount in Model.CustodianAccounts)
{
<li class="nav-item">
<a asp-area="" asp-controller="UICustodianAccounts" asp-action="ViewCustodianAccount" asp-route-storeId="@custodianAccount.StoreId" asp-route-accountId="@custodianAccount.Id" class="nav-link @ViewData.IsActivePage(CustodianAccountsNavPages.View, custodianAccount.Id)" id="@($"StoreNav-CustodianAccount-{custodianAccount.Id}")">
@* TODO which icon should we use? *@
<span>@custodianAccount.Name</span>
<span class="badge bg-warning ms-1" style="font-size:10px;">Experimental</span>
</a>
</li>
}
<li class="nav-item">
<a asp-area="" asp-controller="UICustodianAccounts" asp-action="CreateCustodianAccount" asp-route-storeId="@Model.Store.Id" class="nav-link @ViewData.IsActivePage(CustodianAccountsNavPages.Create)" id="StoreNav-CreateCustodianAccount">
<vc:icon symbol="new"/>
<span>Add Custodian</span>
<span class="badge bg-warning ms-1" style="font-size:10px;">Experimental</span>
</a>
</li>
}
</ul>
</div>
</div>

View File

@ -9,7 +9,6 @@ using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Services;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Custodian.Client;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Identity;
@ -28,7 +27,6 @@ namespace BTCPayServer.Components.MainNav
private readonly BTCPayNetworkProvider _networkProvider;
private readonly UserManager<ApplicationUser> _userManager;
private readonly PaymentMethodHandlerDictionary _paymentMethodHandlerDictionary;
private readonly CustodianAccountRepository _custodianAccountRepository;
private readonly SettingsRepository _settingsRepository;
public PoliciesSettings PoliciesSettings { get; }
@ -39,7 +37,6 @@ namespace BTCPayServer.Components.MainNav
BTCPayNetworkProvider networkProvider,
UserManager<ApplicationUser> userManager,
PaymentMethodHandlerDictionary paymentMethodHandlerDictionary,
CustodianAccountRepository custodianAccountRepository,
SettingsRepository settingsRepository,
PoliciesSettings policiesSettings)
{
@ -49,7 +46,6 @@ namespace BTCPayServer.Components.MainNav
_networkProvider = networkProvider;
_storesController = storesController;
_paymentMethodHandlerDictionary = paymentMethodHandlerDictionary;
_custodianAccountRepository = custodianAccountRepository;
_settingsRepository = settingsRepository;
PoliciesSettings = policiesSettings;
}
@ -88,13 +84,6 @@ namespace BTCPayServer.Components.MainNav
}).ToList();
vm.ArchivedAppsCount = apps.Count(a => a.Archived);
if (PoliciesSettings.Experimental)
{
// Custodian Accounts
var custodianAccounts = await _custodianAccountRepository.FindByStoreId(store.Id);
vm.CustodianAccounts = custodianAccounts;
}
}
return View(vm);

View File

@ -10,7 +10,6 @@ namespace BTCPayServer.Components.MainNav
public List<StoreDerivationScheme> DerivationSchemes { get; set; }
public List<StoreLightningNode> LightningNodes { get; set; }
public List<StoreApp> Apps { get; set; }
public CustodianAccountData[] CustodianAccounts { get; set; }
public bool AltcoinsBuild { get; set; }
public int ArchivedAppsCount { get; set; }
public string ContactUrl { get; set; }

View File

@ -12,6 +12,7 @@ using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Security;
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization;
@ -30,6 +31,7 @@ public class StoreLightningBalance : ViewComponent
private readonly IOptions<LightningNetworkOptions> _lightningNetworkOptions;
private readonly IOptions<ExternalServicesOptions> _externalServiceOptions;
private readonly IAuthorizationService _authorizationService;
private readonly PaymentMethodHandlerDictionary _handlers;
public StoreLightningBalance(
StoreRepository storeRepo,
@ -38,8 +40,9 @@ public class StoreLightningBalance : ViewComponent
BTCPayServerOptions btcpayServerOptions,
LightningClientFactoryService lightningClientFactory,
IOptions<LightningNetworkOptions> lightningNetworkOptions,
IOptions<ExternalServicesOptions> externalServiceOptions,
IAuthorizationService authorizationService)
IOptions<ExternalServicesOptions> externalServiceOptions,
IAuthorizationService authorizationService,
PaymentMethodHandlerDictionary handlers)
{
_storeRepo = storeRepo;
_currencies = currencies;
@ -47,6 +50,7 @@ public class StoreLightningBalance : ViewComponent
_btcpayServerOptions = btcpayServerOptions;
_externalServiceOptions = externalServiceOptions;
_authorizationService = authorizationService;
_handlers = handlers;
_lightningClientFactory = lightningClientFactory;
_lightningNetworkOptions = lightningNetworkOptions;
}
@ -101,10 +105,8 @@ public class StoreLightningBalance : ViewComponent
private async Task<ILightningClient> GetLightningClient(StoreData store, string cryptoCode )
{
var network = _networkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
var id = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike);
var existing = store.GetSupportedPaymentMethods(_networkProvider)
.OfType<LightningSupportedPaymentMethod>()
.FirstOrDefault(d => d.PaymentId == id);
var id = PaymentTypes.LN.GetPaymentMethodId(cryptoCode);
var existing = store.GetPaymentMethodConfig<LightningPaymentMethodConfig>(id, _handlers);
if (existing == null)
return null;

View File

@ -8,7 +8,9 @@ using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Data;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Labels;
using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Wallets;
@ -27,21 +29,20 @@ public class StoreRecentTransactions : ViewComponent
private readonly BTCPayWalletProvider _walletProvider;
private readonly WalletRepository _walletRepository;
private readonly LabelService _labelService;
private readonly PaymentMethodHandlerDictionary _handlers;
private readonly TransactionLinkProviders _transactionLinkProviders;
public BTCPayNetworkProvider NetworkProvider { get; }
public StoreRecentTransactions(
BTCPayNetworkProvider networkProvider,
BTCPayWalletProvider walletProvider,
WalletRepository walletRepository,
LabelService labelService,
PaymentMethodHandlerDictionary handlers,
TransactionLinkProviders transactionLinkProviders)
{
NetworkProvider = networkProvider;
_walletProvider = walletProvider;
_walletRepository = walletRepository;
_labelService = labelService;
_handlers = handlers;
_transactionLinkProviders = transactionLinkProviders;
}
@ -57,15 +58,16 @@ public class StoreRecentTransactions : ViewComponent
if (vm.InitialRendering)
return View(vm);
var derivationSettings = vm.Store.GetDerivationSchemeSettings(NetworkProvider, vm.CryptoCode);
var derivationSettings = vm.Store.GetDerivationSchemeSettings(_handlers, vm.CryptoCode);
var transactions = new List<StoreRecentTransactionViewModel>();
if (derivationSettings?.AccountDerivation is not null)
{
var network = derivationSettings.Network;
var pmi = PaymentTypes.CHAIN.GetPaymentMethodId(vm.CryptoCode);
var network = ((IHasNetwork)_handlers[pmi]).Network;
var wallet = _walletProvider.GetWallet(network);
var allTransactions = await wallet.FetchTransactionHistory(derivationSettings.AccountDerivation, 0, 5, TimeSpan.FromDays(31.0), cancellationToken: this.HttpContext.RequestAborted);
var walletTransactionsInfo = await _walletRepository.GetWalletTransactionsInfo(vm.WalletId, allTransactions.Select(t => t.TransactionId.ToString()).ToArray());
var pmi = new PaymentMethodId(vm.CryptoCode, PaymentTypes.BTCLike);
transactions = allTransactions
.Select(tx =>
{
@ -78,7 +80,7 @@ public class StoreRecentTransactions : ViewComponent
Balance = tx.BalanceChange.ShowMoney(network),
Currency = vm.CryptoCode,
IsConfirmed = tx.Confirmations != 0,
Link = _transactionLinkProviders.GetTransactionLink(pmi, tx.TransactionId.ToString()),
Link = _transactionLinkProviders.GetTransactionLink(network.CryptoCode, tx.TransactionId.ToString()),
Timestamp = tx.SeenAt,
Labels = labels
};

View File

@ -2,6 +2,8 @@ using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Client;
using BTCPayServer.Data;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
@ -12,17 +14,14 @@ namespace BTCPayServer.Components.StoreSelector
public class StoreSelector : ViewComponent
{
private readonly StoreRepository _storeRepo;
private readonly BTCPayNetworkProvider _networkProvider;
private readonly UserManager<ApplicationUser> _userManager;
public StoreSelector(
StoreRepository storeRepo,
BTCPayNetworkProvider networkProvider,
UserManager<ApplicationUser> userManager)
{
_storeRepo = storeRepo;
_userManager = userManager;
_networkProvider = networkProvider;
}
public async Task<IViewComponentResult> InvokeAsync()
@ -34,21 +33,12 @@ namespace BTCPayServer.Components.StoreSelector
var options = stores
.Where(store => !store.Archived)
.Select(store =>
new StoreSelectorOption
{
var cryptoCode = store
.GetSupportedPaymentMethods(_networkProvider)
.OfType<DerivationSchemeSettings>()
.FirstOrDefault()?
.Network.CryptoCode;
var walletId = cryptoCode != null ? new WalletId(store.Id, cryptoCode) : null;
return new StoreSelectorOption
{
Text = store.StoreName,
Value = store.Id,
Selected = store.Id == currentStore?.Id,
WalletId = walletId,
Store = store
};
Text = store.StoreName,
Value = store.Id,
Selected = store.Id == currentStore?.Id,
Store = store
})
.OrderBy(s => s.Text)
.ToList();

View File

@ -17,7 +17,6 @@ namespace BTCPayServer.Components.StoreSelector
public bool Selected { get; set; }
public string Text { get; set; }
public string Value { get; set; }
public WalletId WalletId { get; set; }
public StoreData Store { get; set; }
}
}

Some files were not shown because too many files have changed in this diff Show More