Compare commits
50 Commits
v1.8.4
...
lnurl-hook
Author | SHA1 | Date | |
---|---|---|---|
72e66aa576 | |||
6388057806 | |||
1f197f6688 | |||
1055e61bb4 | |||
d3f5576570 | |||
45141d1391 | |||
de9ac9fd43 | |||
c53d5272d6 | |||
18c78192ec | |||
632d67eef4 | |||
c23aa48688 | |||
95f3e429b4 | |||
8635fcfe84 | |||
d861537d9a | |||
631ee99f60 | |||
ffa1441ccd | |||
2f3e947027 | |||
a62aecfdfe | |||
5f829c68f2 | |||
0290d74aeb | |||
f6bc16007d | |||
ad5752f09b | |||
55565f1718 | |||
5f96d17b8c | |||
fd22406e0a | |||
64fe542c1e | |||
fae1dc8dbb | |||
6f2b673021 | |||
b26679ca14 | |||
04ba1430ca | |||
53f3758abc | |||
c6742f5533 | |||
cb44591a47 | |||
e02abb509f | |||
eff6be9643 | |||
348dbd7107 | |||
f74ea14d8b | |||
a671632fde | |||
e344622c9e | |||
06d7483ca3 | |||
7fe041fc2c | |||
3f18e5476a | |||
2a31613fe8 | |||
ded0c8a3bc | |||
f3d9e07c5e | |||
eb3ba95114 | |||
7951dcada6 | |||
06951a39c6 | |||
abe29f21f0 | |||
f57eab3008 |
.github/ISSUE_TEMPLATE
BTCPayServer.Abstractions
BTCPayServer.Client
BTCPayServerClient.CustodianAccounts.cs
JsonConverters
Models
LedgerEntryData.csTradeQuoteResponseData.csWithdrawRequestData.csWithdrawalBaseResponseData.csWithdrawalResponseData.csWithdrawalSimulationResponseData.cs
Permissions.csBTCPayServer.Data
BTCPayServer.Rating
BTCPayServer.Tests
AltcoinTests
CheckoutUITests.csCheckoutv2Tests.csCrowdfundTests.csFastTests.csGreenfieldAPITests.csMockCustodian
POSTests.csPayJoinTests.csSeleniumTests.csTestAccount.csThirdPartyTests.csUnitTest1.csUtilitiesTests.csBTCPayServer
BTCPayServer.csprojColorPalette.cs
Components
AppSales
AppTopItems
LabelManager
MainNav
Notifications
StoreRecentInvoices
StoreRecentTransactions
TruncateCenter
Configuration
Controllers
GreenField
GreenfieldAppsController.csGreenfieldCustodianAccountController.csGreenfieldLightningNodeApiController.csGreenfieldPayoutProcessorsController.csGreenfieldStoreAutomatedLightningPayoutProcessorsController.csGreenfieldStoreAutomatedOnChainPayoutProcessorsController.csGreenfieldStoreLNURLPayPaymentMethodsController.csGreenfieldStoreLightningAddressesController.csGreenfieldStoreLightningNetworkPaymentMethodsController.csGreenfieldStoreOnChainPaymentMethodsController.WalletGeneration.csGreenfieldStoreOnChainPaymentMethodsController.csGreenfieldStorePaymentMethodsController.csGreenfieldStorePayoutProcessorsController.csGreenfieldStoreRatesConfigurationController.csGreenfieldStoreRatesController.csGreenfieldTestApiKeyController.csLocalBTCPayServerClient.cs
UIAccountController.csUIAppsController.Dashboard.csUIAppsController.csUICustodianAccountsController.csUIHomeController.csUIInvoiceController.Testing.csUIInvoiceController.UI.csUIInvoiceController.csUILNURLController.csUIPaymentRequestController.csUIPublicController.csUIPullPaymentController.csUIServerController.csUIStorePullPaymentsController.PullPayments.csUIStoresController.Dashboard.csUIStoresController.Integrations.csUIStoresController.csUIWalletsController.PSBT.csUIWalletsController.csData
DerivationSchemeSettings.csFilters
Forms
HostedServices
Hosting
Models
AccountViewModels
AppViewModels
CustodianAccountViewModels
InvoicingModels
PaymentRequestViewModels
StoreViewModels
ViewPullPaymentModel.csWalletViewModels
PaymentRequest
Payments
PayoutProcessors
Plugins
Crowdfund
NFC
PointOfSale
Shopify
Security
Services
Views
Shared
Bitcoin
Crowdfund
Forms
LNURL
LayoutHeadStoreBranding.cshtmlLightning
NFC
PayButton
PointOfSale
Shopify
ShowQR.cshtml_Footer.cshtml_Layout.cshtmlUIAccount
UIApps
UICustodianAccounts
UIForms
UIHome
UIInvoice
Checkout-Body.cshtmlCheckout-Cheating.cshtmlCheckout.cshtmlCheckoutV2.cshtmlInvoice.cshtmlInvoiceReceipt.cshtmlListInvoices.cshtmlListInvoicesPaymentsPartial.cshtml
UILNURL
UIManage
UIPublicLightningNodeInfo
UIPullPayment
UIStores
CheckoutAppearance.cshtmlDashboard.cshtmlGeneralSettings.cshtml
ImportWallet
Plugins.cshtmlStoreNavPages.csWalletSettings.cshtml_Nav.cshtmlUIWallets
wwwroot
checkout-v2
img
js
locales/checkout
am-ET.jsonar.jsonaz.jsonbg-BG.jsonbs-BA.jsonca-ES.jsoncs-CZ.jsonda-DK.jsonde-DE.jsonel-GR.jsonen.jsones-ES.jsonfa.jsonfi-FI.jsonfr-FR.jsonhe.jsonhi.jsonhr-HR.jsonhu-HU.jsonhy.jsonid.jsonis-IS.jsonit-IT.jsonja-JP.jsonka.jsonkk-KZ.jsonko.jsonlv.jsonnl-NL.jsonno.jsonnp-NP.jsonpl.jsonpt-BR.jsonpt-PT.jsonro.jsonru-RU.jsonsk-SK.jsonsl-SI.jsonsr.jsonsv.jsonth-TH.jsontr.jsonuk-UA.jsonvi-VN.jsonzh-SG.jsonzh-SP.jsonzh-TW.jsonzu.json
main
swagger/v1
vendor/dom-confetti
Build
Changelog.mdPlugins/BTCPayServer.Plugins.Custodians.FakeCustodian
12
.github/ISSUE_TEMPLATE/config.yml
vendored
12
.github/ISSUE_TEMPLATE/config.yml
vendored
@ -1,8 +1,14 @@
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: 🚀 Discussions
|
||||
url: https://github.com/btcpayserver/btcpayserver/discussions
|
||||
about: Technical discussions, questions and feature requests
|
||||
- name: 💡 Request a feature
|
||||
url: https://github.com/btcpayserver/btcpayserver/discussions/categories/ideas-feature-requests
|
||||
about: Submit a feature request or vote on ideas posted by others. Features with most upvotes become roadmap candidates
|
||||
- name: 🧑💻 Ask a technical question
|
||||
url: https://github.com/btcpayserver/btcpayserver/discussions/new?category=technical-support
|
||||
about: If you're experiencing a technical problem post it to our community support forum
|
||||
- name: 🔌 Report a problem with a plugin
|
||||
url: https://github.com/btcpayserver/btcpayserver/discussions/new?category=plugins-integrations
|
||||
about: Experiencing a problem with a third-party plugin? Post it here and we will tag their developers to assist
|
||||
- name: 📝 Official Documentation
|
||||
url: https://docs.btcpayserver.org
|
||||
about: Check our documentation for answers to common questions
|
||||
|
@ -5,4 +5,8 @@ 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}")
|
||||
{
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,28 @@
|
||||
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;
|
||||
}
|
||||
}
|
@ -5,9 +5,14 @@ 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> WithdrawAsync(string paymentMethod, decimal amount, JObject config, CancellationToken cancellationToken);
|
||||
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);
|
||||
|
||||
|
@ -20,7 +20,6 @@ public interface ICustodian
|
||||
*/
|
||||
Task<Dictionary<string, decimal>> GetAssetBalancesAsync(JObject config, CancellationToken cancellationToken);
|
||||
|
||||
public Task<Form.Form> GetConfigForm(JObject config, string locale,
|
||||
CancellationToken cancellationToken = default);
|
||||
public Task<Form.Form> GetConfigForm(CancellationToken cancellationToken = default);
|
||||
|
||||
}
|
||||
|
@ -28,7 +28,7 @@ public class Field
|
||||
|
||||
public bool Constant;
|
||||
|
||||
// HTML5 compatible type string like "text", "textarea", "email", "password", etc. Each type is a class and may contain more fields (i.e. "select" would have options).
|
||||
// HTML5 compatible type string like "text", "textarea", "email", "password", etc.
|
||||
public string Type;
|
||||
|
||||
public static Field CreateFieldset()
|
||||
|
@ -50,7 +50,7 @@ public class Form
|
||||
HashSet<string> nameReturned = new();
|
||||
foreach (var f in GetAllFieldsCore(new List<string>(), Fields))
|
||||
{
|
||||
var fullName = string.Join('_', f.Path);
|
||||
var fullName = string.Join('_', f.Path.Where(s => !string.IsNullOrEmpty(s)));
|
||||
if (!nameReturned.Add(fullName))
|
||||
continue;
|
||||
yield return (fullName, f.Path, f.Field);
|
||||
@ -63,7 +63,7 @@ public class Form
|
||||
HashSet<string> nameReturned = new();
|
||||
foreach (var f in GetAllFieldsCore(new List<string>(), Fields))
|
||||
{
|
||||
var fullName = string.Join('_', f.Path);
|
||||
var fullName = string.Join('_', f.Path.Where(s => !string.IsNullOrEmpty(s)));
|
||||
if (!nameReturned.Add(fullName))
|
||||
{
|
||||
errors.Add($"Form contains duplicate field names '{fullName}'");
|
||||
@ -128,8 +128,8 @@ public class Form
|
||||
}
|
||||
else if (prop.Value.Type == JTokenType.String)
|
||||
{
|
||||
var fullname = String.Join('_', propPath);
|
||||
if (fields.TryGetValue(fullname, out var f) && !f.Constant)
|
||||
var fullName = string.Join('_', propPath.Where(s => !string.IsNullOrEmpty(s)));
|
||||
if (fields.TryGetValue(fullName, out var f) && !f.Constant)
|
||||
f.Value = prop.Value.Value<string>();
|
||||
}
|
||||
}
|
||||
|
@ -50,7 +50,7 @@ namespace BTCPayServer.Client
|
||||
await HandleResponse(response);
|
||||
}
|
||||
|
||||
public virtual async Task<DepositAddressData> GetDepositAddress(string storeId, string accountId, string paymentMethod, CancellationToken token = default)
|
||||
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);
|
||||
@ -58,7 +58,6 @@ namespace BTCPayServer.Client
|
||||
|
||||
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,
|
||||
@ -67,13 +66,13 @@ namespace BTCPayServer.Client
|
||||
return await HandleResponse<MarketTradeResponseData>(response);
|
||||
}
|
||||
|
||||
public virtual async Task<MarketTradeResponseData> GetTradeInfo(string storeId, string accountId, string tradeId, CancellationToken token = default)
|
||||
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> GetTradeQuote(string storeId, string accountId, string fromAsset, string toAsset, CancellationToken token = default)
|
||||
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);
|
||||
@ -81,14 +80,20 @@ namespace BTCPayServer.Client
|
||||
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> CreateWithdrawal(string storeId, string accountId, WithdrawRequestData request, CancellationToken token = default)
|
||||
|
||||
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> GetWithdrawalInfo(string storeId, string accountId, string paymentMethod, string withdrawalId, CancellationToken token = default)
|
||||
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);
|
||||
|
@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Lightning;
|
||||
using NBitcoin.JsonConverters;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Client.JsonConverters
|
||||
{
|
||||
public class TradeQuantityJsonConverter : JsonConverter<TradeQuantity>
|
||||
{
|
||||
public override TradeQuantity ReadJson(JsonReader reader, Type objectType, TradeQuantity existingValue, bool hasExistingValue, JsonSerializer serializer)
|
||||
{
|
||||
if (reader.TokenType == JsonToken.Null)
|
||||
return null;
|
||||
if (reader.TokenType != JsonToken.String)
|
||||
throw new JsonObjectException("Invalid TradeQuantity, expected string. Expected: \"1.50\" or \"50%\"", reader);
|
||||
if (TradeQuantity.TryParse((string)reader.Value, out var q))
|
||||
return q;
|
||||
throw new JsonObjectException("Invalid format for TradeQuantity. 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());
|
||||
}
|
||||
}
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
using BTCPayServer.JsonConverters;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
|
||||
@ -6,6 +7,7 @@ namespace BTCPayServer.Client.Models;
|
||||
public class LedgerEntryData
|
||||
{
|
||||
public string Asset { get; }
|
||||
[JsonConverter(typeof(NumericStringJsonConverter))]
|
||||
public decimal Qty { get; }
|
||||
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
|
@ -1,8 +1,13 @@
|
||||
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; }
|
||||
|
@ -1,13 +1,85 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
using System.Net.Http.Headers;
|
||||
|
||||
namespace BTCPayServer.Client.Models;
|
||||
|
||||
public class WithdrawRequestData
|
||||
{
|
||||
public string PaymentMethod { set; get; }
|
||||
public decimal Qty { set; get; }
|
||||
[JsonConverter(typeof(JsonConverters.TradeQuantityJsonConverter))]
|
||||
public TradeQuantity Qty { set; get; }
|
||||
|
||||
public WithdrawRequestData(string paymentMethod, decimal qty)
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
22
BTCPayServer.Client/Models/WithdrawalBaseResponseData.cs
Normal file
22
BTCPayServer.Client/Models/WithdrawalBaseResponseData.cs
Normal file
@ -0,0 +1,22 @@
|
||||
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;
|
||||
}
|
||||
}
|
@ -5,18 +5,13 @@ using Newtonsoft.Json.Converters;
|
||||
|
||||
namespace BTCPayServer.Client.Models;
|
||||
|
||||
public class WithdrawalResponseData
|
||||
public class WithdrawalResponseData : WithdrawalBaseResponseData
|
||||
{
|
||||
public string Asset { get; }
|
||||
public string PaymentMethod { get; }
|
||||
public List<LedgerEntryData> LedgerEntries { get; }
|
||||
public string WithdrawalId { get; }
|
||||
public string AccountId { get; }
|
||||
public string CustodianCode { get; }
|
||||
|
||||
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public WithdrawalStatus Status { get; }
|
||||
|
||||
public string WithdrawalId { get; }
|
||||
public DateTimeOffset CreatedTime { get; }
|
||||
|
||||
public string TransactionId { get; }
|
||||
@ -24,14 +19,10 @@ public class WithdrawalResponseData
|
||||
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)
|
||||
string custodianCode, WithdrawalStatus status, DateTimeOffset createdTime, string targetAddress, string transactionId) : base(paymentMethod, asset, ledgerEntries, accountId,
|
||||
custodianCode)
|
||||
{
|
||||
PaymentMethod = paymentMethod;
|
||||
Asset = asset;
|
||||
LedgerEntries = ledgerEntries;
|
||||
WithdrawalId = withdrawalId;
|
||||
AccountId = accountId;
|
||||
CustodianCode = custodianCode;
|
||||
TargetAddress = targetAddress;
|
||||
TransactionId = transactionId;
|
||||
Status = status;
|
||||
|
@ -0,0 +1,21 @@
|
||||
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;
|
||||
}
|
||||
}
|
@ -98,6 +98,37 @@ namespace BTCPayServer.Client
|
||||
{
|
||||
return policy.StartsWith("btcpay.plugin", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
public static bool IsUserPolicy(string policy)
|
||||
{
|
||||
return policy.StartsWith("btcpay.user", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
public class PermissionSet
|
||||
{
|
||||
public PermissionSet() : this(Array.Empty<Permission>())
|
||||
{
|
||||
|
||||
}
|
||||
public PermissionSet(Permission[] permissions)
|
||||
{
|
||||
Permissions = permissions;
|
||||
}
|
||||
|
||||
public Permission[] Permissions { get; }
|
||||
|
||||
public bool Contains(Permission requestedPermission)
|
||||
{
|
||||
return Permissions.Any(p => p.Contains(requestedPermission));
|
||||
}
|
||||
public bool Contains(string permission, string store)
|
||||
{
|
||||
if (permission is null)
|
||||
throw new ArgumentNullException(nameof(permission));
|
||||
if (store is null)
|
||||
throw new ArgumentNullException(nameof(store));
|
||||
return Contains(Permission.Create(permission, store));
|
||||
}
|
||||
}
|
||||
public class Permission
|
||||
{
|
||||
@ -105,7 +136,7 @@ namespace BTCPayServer.Client
|
||||
{
|
||||
Init();
|
||||
}
|
||||
|
||||
|
||||
public static Permission Create(string policy, string scope = null)
|
||||
{
|
||||
if (TryCreatePermission(policy, scope, out var r))
|
||||
@ -121,7 +152,7 @@ namespace BTCPayServer.Client
|
||||
policy = policy.Trim().ToLowerInvariant();
|
||||
if (!Policies.IsValidPolicy(policy))
|
||||
return false;
|
||||
if (scope != null && !Policies.IsStorePolicy(policy))
|
||||
if (!string.IsNullOrEmpty(scope) && !Policies.IsStorePolicy(policy))
|
||||
return false;
|
||||
permission = new Permission(policy, scope);
|
||||
return true;
|
||||
@ -174,7 +205,7 @@ namespace BTCPayServer.Client
|
||||
}
|
||||
if (!Policies.IsStorePolicy(subpermission.Policy))
|
||||
return true;
|
||||
return Scope == null || subpermission.Scope == this.Scope;
|
||||
return Scope == null || subpermission.Scope == Scope;
|
||||
}
|
||||
|
||||
public static IEnumerable<Permission> ToPermissions(string[] permissions)
|
||||
@ -199,7 +230,8 @@ namespace BTCPayServer.Client
|
||||
return true;
|
||||
if (policy == subpolicy)
|
||||
return true;
|
||||
if (!PolicyMap.TryGetValue(policy, out var subPolicies)) return false;
|
||||
if (!PolicyMap.TryGetValue(policy, out var subPolicies))
|
||||
return false;
|
||||
return subPolicies.Contains(subpolicy) || subPolicies.Any(s => ContainsPolicy(s, subpolicy));
|
||||
}
|
||||
|
||||
@ -213,23 +245,23 @@ namespace BTCPayServer.Client
|
||||
Policies.CanModifyInvoices,
|
||||
Policies.CanViewStoreSettings,
|
||||
Policies.CanModifyStoreWebhooks,
|
||||
Policies.CanModifyPaymentRequests);
|
||||
Policies.CanModifyPaymentRequests,
|
||||
Policies.CanUseLightningNodeInStore);
|
||||
|
||||
PolicyHasChild(Policies.CanManageUsers, Policies.CanCreateUser);
|
||||
PolicyHasChild(Policies.CanManagePullPayments, Policies.CanCreatePullPayments );
|
||||
PolicyHasChild(Policies.CanCreatePullPayments, Policies.CanCreateNonApprovedPullPayments );
|
||||
PolicyHasChild(Policies.CanModifyPaymentRequests, Policies.CanViewPaymentRequests );
|
||||
PolicyHasChild(Policies.CanModifyProfile, Policies.CanViewProfile );
|
||||
PolicyHasChild(Policies.CanUseLightningNodeInStore, Policies.CanViewLightningInvoiceInStore, Policies.CanCreateLightningInvoiceInStore );
|
||||
PolicyHasChild(Policies.CanManageNotificationsForUser, Policies.CanViewNotificationsForUser );
|
||||
PolicyHasChild(Policies.CanManagePullPayments, Policies.CanCreatePullPayments);
|
||||
PolicyHasChild(Policies.CanCreatePullPayments, Policies.CanCreateNonApprovedPullPayments);
|
||||
PolicyHasChild(Policies.CanModifyPaymentRequests, Policies.CanViewPaymentRequests);
|
||||
PolicyHasChild(Policies.CanModifyProfile, Policies.CanViewProfile);
|
||||
PolicyHasChild(Policies.CanUseLightningNodeInStore, Policies.CanViewLightningInvoiceInStore, Policies.CanCreateLightningInvoiceInStore);
|
||||
PolicyHasChild(Policies.CanManageNotificationsForUser, Policies.CanViewNotificationsForUser);
|
||||
PolicyHasChild(Policies.CanModifyServerSettings,
|
||||
Policies.CanUseInternalLightningNode,
|
||||
Policies.CanManageUsers);
|
||||
PolicyHasChild(Policies.CanUseInternalLightningNode, Policies.CanCreateLightningInvoiceInternalNode,Policies.CanViewLightningInvoiceInternalNode );
|
||||
PolicyHasChild(Policies.CanManageCustodianAccounts, Policies.CanViewCustodianAccounts );
|
||||
PolicyHasChild(Policies.CanModifyInvoices, Policies.CanViewInvoices, Policies.CanCreateInvoice );
|
||||
PolicyHasChild(Policies.CanViewStoreSettings, Policies.CanViewInvoices, Policies.CanViewPaymentRequests );
|
||||
|
||||
PolicyHasChild(Policies.CanUseInternalLightningNode, Policies.CanCreateLightningInvoiceInternalNode, Policies.CanViewLightningInvoiceInternalNode);
|
||||
PolicyHasChild(Policies.CanManageCustodianAccounts, Policies.CanViewCustodianAccounts);
|
||||
PolicyHasChild(Policies.CanModifyInvoices, Policies.CanViewInvoices, Policies.CanCreateInvoice, Policies.CanCreateLightningInvoiceInStore);
|
||||
PolicyHasChild(Policies.CanViewStoreSettings, Policies.CanViewInvoices, Policies.CanViewPaymentRequests);
|
||||
}
|
||||
|
||||
private static void PolicyHasChild(string policy, params string[] subPolicies)
|
||||
@ -243,33 +275,26 @@ namespace BTCPayServer.Client
|
||||
}
|
||||
else
|
||||
{
|
||||
PolicyMap.Add(policy,subPolicies.ToHashSet());
|
||||
PolicyMap.Add(policy, subPolicies.ToHashSet());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public string Scope { get; }
|
||||
public string Policy { get; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
if (Scope != null)
|
||||
{
|
||||
return $"{Policy}:{Scope}";
|
||||
}
|
||||
return Policy;
|
||||
return Scope != null ? $"{Policy}:{Scope}" : Policy;
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
Permission item = obj as Permission;
|
||||
if (item == null)
|
||||
return false;
|
||||
return ToString().Equals(item.ToString());
|
||||
return item != null && ToString().Equals(item.ToString());
|
||||
}
|
||||
public static bool operator ==(Permission a, Permission b)
|
||||
{
|
||||
if (System.Object.ReferenceEquals(a, b))
|
||||
if (ReferenceEquals(a, b))
|
||||
return true;
|
||||
if (((object)a == null) || ((object)b == null))
|
||||
return false;
|
||||
|
@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Text;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
|
31
BTCPayServer.Data/Migrations/20230315062447_fixmaxlength.cs
Normal file
31
BTCPayServer.Data/Migrations/20230315062447_fixmaxlength.cs
Normal file
@ -0,0 +1,31 @@
|
||||
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("20230315062447_fixmaxlength")]
|
||||
public partial class fixmaxlength : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
if (migrationBuilder.IsNpgsql())
|
||||
{
|
||||
migrationBuilder.Sql("ALTER TABLE \"InvoiceSearches\" ALTER COLUMN \"Value\" TYPE TEXT USING \"Value\"::TEXT;");
|
||||
migrationBuilder.Sql("ALTER TABLE \"Invoices\" ALTER COLUMN \"OrderId\" TYPE TEXT USING \"OrderId\"::TEXT;");
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
// Not supported
|
||||
}
|
||||
}
|
||||
}
|
@ -1305,7 +1305,7 @@
|
||||
"name":"Satoshis",
|
||||
"code":"SATS",
|
||||
"divisibility":0,
|
||||
"symbol":"Sats",
|
||||
"symbol":"sats",
|
||||
"crypto":true
|
||||
},
|
||||
{
|
||||
|
@ -5,7 +5,6 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using BTCPayServer.Rating;
|
||||
using NBitcoin;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
@ -28,14 +27,6 @@ namespace BTCPayServer.Services.Rates
|
||||
}
|
||||
|
||||
static readonly Dictionary<string, IFormatProvider> _CurrencyProviders = new Dictionary<string, IFormatProvider>();
|
||||
public string FormatCurrency(string price, string currency)
|
||||
{
|
||||
return FormatCurrency(decimal.Parse(price, CultureInfo.InvariantCulture), currency);
|
||||
}
|
||||
public string FormatCurrency(decimal price, string currency)
|
||||
{
|
||||
return price.ToString("C", GetCurrencyProvider(currency));
|
||||
}
|
||||
|
||||
public NumberFormatInfo GetNumberFormatInfo(string currency, bool useFallback)
|
||||
{
|
||||
@ -56,6 +47,7 @@ namespace BTCPayServer.Services.Rates
|
||||
currencyInfo.CurrencySymbol = currency;
|
||||
return currencyInfo;
|
||||
}
|
||||
|
||||
public NumberFormatInfo GetNumberFormatInfo(string currency)
|
||||
{
|
||||
var curr = GetCurrencyProvider(currency);
|
||||
@ -65,6 +57,7 @@ namespace BTCPayServer.Services.Rates
|
||||
return ni;
|
||||
return null;
|
||||
}
|
||||
|
||||
public IFormatProvider GetCurrencyProvider(string currency)
|
||||
{
|
||||
lock (_CurrencyProviders)
|
||||
@ -104,30 +97,6 @@ namespace BTCPayServer.Services.Rates
|
||||
currencyProviders.TryAdd(code, number);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Format a currency like "0.004 $ (USD)", round to significant divisibility
|
||||
/// </summary>
|
||||
/// <param name="value">The value</param>
|
||||
/// <param name="currency">Currency code</param>
|
||||
/// <returns></returns>
|
||||
public string DisplayFormatCurrency(decimal value, string currency)
|
||||
{
|
||||
var provider = GetNumberFormatInfo(currency, true);
|
||||
var currencyData = GetCurrencyData(currency, true);
|
||||
var divisibility = currencyData.Divisibility;
|
||||
value = value.RoundToSignificant(ref divisibility);
|
||||
if (divisibility != provider.CurrencyDecimalDigits)
|
||||
{
|
||||
provider = (NumberFormatInfo)provider.Clone();
|
||||
provider.CurrencyDecimalDigits = divisibility;
|
||||
}
|
||||
|
||||
if (currencyData.Crypto)
|
||||
return value.ToString("C", provider);
|
||||
else
|
||||
return value.ToString("C", provider) + $" ({currency})";
|
||||
}
|
||||
|
||||
readonly Dictionary<string, CurrencyData> _Currencies;
|
||||
|
||||
static CurrencyData[] LoadCurrency()
|
||||
|
@ -11,6 +11,7 @@ using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Models.WalletViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Plugins.PointOfSale;
|
||||
using BTCPayServer.Plugins.PointOfSale.Controllers;
|
||||
using BTCPayServer.Plugins.PointOfSale.Models;
|
||||
using BTCPayServer.Services.Apps;
|
||||
@ -386,7 +387,7 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.FindElement(By.Id("BOLT11Expiration")).SendKeys("5" + Keys.Enter);
|
||||
s.GoToInvoice(invoice.Id);
|
||||
s.Driver.FindElement(By.Id("IssueRefund")).Click();
|
||||
|
||||
|
||||
if (multiCurrency)
|
||||
{
|
||||
s.Driver.WaitUntilAvailable(By.Id("RefundForm"), TimeSpan.FromSeconds(1));
|
||||
@ -396,21 +397,21 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
|
||||
s.Driver.WaitUntilAvailable(By.Id("RefundForm"), TimeSpan.FromSeconds(1));
|
||||
Assert.Contains("$5,500.00", s.Driver.PageSource); // Should propose reimburse in fiat
|
||||
Assert.Contains("1.10000000 ₿", s.Driver.PageSource); // Should propose reimburse in BTC at the rate of before
|
||||
Assert.Contains("2.20000000 ₿", s.Driver.PageSource); // Should propose reimburse in BTC at the current rate
|
||||
Assert.Contains("5,500.00 USD", s.Driver.PageSource); // Should propose reimburse in fiat
|
||||
Assert.Contains("1.10000000 BTC", s.Driver.PageSource); // Should propose reimburse in BTC at the rate of before
|
||||
Assert.Contains("2.20000000 BTC", s.Driver.PageSource); // Should propose reimburse in BTC at the current rate
|
||||
s.Driver.WaitForAndClick(By.Id(rateSelection));
|
||||
s.Driver.FindElement(By.Id("ok")).Click();
|
||||
|
||||
|
||||
s.Driver.WaitUntilAvailable(By.Id("Destination"), TimeSpan.FromSeconds(1));
|
||||
Assert.Contains("pull-payments", s.Driver.Url);
|
||||
if (rateSelection == "FiatOption")
|
||||
Assert.Contains("$5,500.00", s.Driver.PageSource);
|
||||
Assert.Contains("5,500.00 USD", s.Driver.PageSource);
|
||||
if (rateSelection == "CurrentOption")
|
||||
Assert.Contains("2.20000000 ₿", s.Driver.PageSource);
|
||||
Assert.Contains("2.20000000 BTC", s.Driver.PageSource);
|
||||
if (rateSelection == "RateThenOption")
|
||||
Assert.Contains("1.10000000 ₿", s.Driver.PageSource);
|
||||
|
||||
Assert.Contains("1.10000000 BTC", s.Driver.PageSource);
|
||||
|
||||
s.GoToInvoice(invoice.Id);
|
||||
s.Driver.FindElement(By.Id("IssueRefund")).Click();
|
||||
s.Driver.WaitUntilAvailable(By.Id("Destination"), TimeSpan.FromSeconds(1));
|
||||
@ -584,7 +585,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.True(invoice.SupportedTransactionCurrencies["LTC"].Enabled);
|
||||
Assert.True(invoice.PaymentSubtotals.ContainsKey("LTC"));
|
||||
Assert.True(invoice.PaymentTotals.ContainsKey("LTC"));
|
||||
|
||||
|
||||
// Check if we can disable LTC
|
||||
invoice = await user.BitPay.CreateInvoiceAsync(
|
||||
new Invoice
|
||||
@ -622,10 +623,11 @@ namespace BTCPayServer.Tests
|
||||
var apps = user.GetController<UIAppsController>();
|
||||
var pos = user.GetController<UIPointOfSaleController>();
|
||||
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp(user.StoreId)).Model);
|
||||
var appType = AppType.PointOfSale.ToString();
|
||||
var appType = PointOfSaleAppType.AppType;
|
||||
vm.AppName = "test";
|
||||
vm.SelectedAppType = appType;
|
||||
Assert.IsType<RedirectToActionResult>(apps.CreateApp(user.StoreId, vm).Result);
|
||||
var redirect = Assert.IsType<RedirectResult>(apps.CreateApp(user.StoreId, vm).Result);
|
||||
Assert.EndsWith("/settings/pos", redirect.Url);
|
||||
var appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
|
||||
var app = appList.Apps[0];
|
||||
var appData = new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName, AppType = appType };
|
||||
@ -680,7 +682,7 @@ donation:
|
||||
var appleInvoice = invoices.SingleOrDefault(invoice => invoice.ItemCode.Equals("apple"));
|
||||
Assert.NotNull(appleInvoice);
|
||||
Assert.Equal("good apple", appleInvoice.ItemDesc);
|
||||
|
||||
|
||||
// testing custom amount
|
||||
var action = Assert.IsType<RedirectToActionResult>(publicApps
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 6.6m, null, null, null, null, "donation").Result);
|
||||
@ -735,7 +737,7 @@ donation:
|
||||
Assert.Equal(test.ExpectedDivisibility, vmview.CurrencyInfo.Divisibility);
|
||||
Assert.Equal(test.ExpectedSymbolSpace, vmview.CurrencyInfo.SymbolSpace);
|
||||
}
|
||||
|
||||
|
||||
//test inventory related features
|
||||
vmpos = await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
|
||||
vmpos.Title = "hello";
|
||||
@ -756,7 +758,7 @@ noninventoryitem:
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 1, null, null, null, null, "inventoryitem").Result);
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
|
||||
//we already bought all available stock so this should fail
|
||||
await Task.Delay(100);
|
||||
Assert.IsType<RedirectToActionResult>(publicApps
|
||||
@ -819,13 +821,13 @@ normal:
|
||||
normalInvoice.CryptoInfo,
|
||||
s => PaymentTypes.BTCLike.ToString() == s.PaymentType && new[] { "BTC", "LTC" }.Contains(
|
||||
s.CryptoCode));
|
||||
|
||||
|
||||
//test topup option
|
||||
vmpos.Template = @"
|
||||
a:
|
||||
price: 1000.0
|
||||
title: good apple
|
||||
|
||||
|
||||
b:
|
||||
price: 10.0
|
||||
custom: false
|
||||
@ -843,7 +845,7 @@ f:
|
||||
g:
|
||||
custom: topup
|
||||
";
|
||||
|
||||
|
||||
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
|
||||
vmpos = await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
|
||||
Assert.DoesNotContain("custom", vmpos.Template);
|
||||
@ -855,7 +857,7 @@ g:
|
||||
Assert.Contains(items, item => item.Id == "e" && item.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Minimum);
|
||||
Assert.Contains(items, item => item.Id == "f" && item.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Topup);
|
||||
Assert.Contains(items, item => item.Id == "g" && item.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Topup);
|
||||
|
||||
|
||||
Assert.IsType<RedirectToActionResult>(publicApps
|
||||
.ViewPointOfSale(app.Id, PosViewType.Static, null, null, null, null, null, "g").Result);
|
||||
invoices = user.BitPay.GetInvoices();
|
||||
|
@ -182,7 +182,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
var invoiceId = s.CreateInvoice(10, "USD", "a@g.com");
|
||||
s.GoToInvoiceCheckout(invoiceId);
|
||||
Assert.Contains("Sats", s.Driver.FindElement(By.ClassName("payment__currencies_noborder")).Text);
|
||||
Assert.Contains("sats", s.Driver.FindElement(By.ClassName("payment__currencies_noborder")).Text);
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
|
@ -73,6 +73,14 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.ElementDoesNotExist(By.Id("Lightning_BTC"));
|
||||
s.Driver.ElementDoesNotExist(By.Id("PayByLNURL"));
|
||||
|
||||
// Details should show exchange rate
|
||||
s.Driver.ToggleCollapse("PaymentDetails");
|
||||
s.Driver.ElementDoesNotExist(By.Id("PaymentDetails-TotalPrice"));
|
||||
s.Driver.ElementDoesNotExist(By.Id("PaymentDetails-TotalFiat"));
|
||||
s.Driver.ElementDoesNotExist(By.Id("PaymentDetails-AmountDue"));
|
||||
Assert.Contains("$", s.Driver.FindElement(By.Id("PaymentDetails-ExchangeRate")).Text);
|
||||
Assert.Contains("sat/byte", s.Driver.FindElement(By.Id("PaymentDetails-RecommendedFee")).Text);
|
||||
|
||||
// Switch to LNURL
|
||||
s.Driver.FindElement(By.CssSelector(".payment-method:nth-child(2)")).Click();
|
||||
TestUtils.Eventually(() =>
|
||||
@ -86,7 +94,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
// Default payment method
|
||||
s.GoToHome();
|
||||
invoiceId = s.CreateInvoice(defaultPaymentMethod: "BTC_LightningLike");
|
||||
invoiceId = s.CreateInvoice(21000, "SATS", defaultPaymentMethod: "BTC_LightningLike");
|
||||
s.GoToInvoiceCheckout(invoiceId);
|
||||
s.Driver.WaitUntilAvailable(By.Id("Checkout-v2"));
|
||||
Assert.Equal(2, s.Driver.FindElements(By.CssSelector(".payment-method")).Count);
|
||||
@ -102,7 +110,7 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.ElementDoesNotExist(By.Id("Address_BTC"));
|
||||
s.Driver.FindElement(By.Id("PayByLNURL"));
|
||||
|
||||
// Lightning amount in Sats
|
||||
// Lightning amount in sats
|
||||
Assert.Contains("BTC", s.Driver.FindElement(By.Id("AmountDue")).Text);
|
||||
s.GoToHome();
|
||||
s.GoToLightningSettings();
|
||||
@ -111,7 +119,15 @@ namespace BTCPayServer.Tests
|
||||
Assert.Contains("BTC Lightning settings successfully updated", s.FindAlertMessage().Text);
|
||||
s.GoToInvoiceCheckout(invoiceId);
|
||||
s.Driver.WaitUntilAvailable(By.Id("Checkout-v2"));
|
||||
Assert.Contains("Sats", s.Driver.FindElement(By.Id("AmountDue")).Text);
|
||||
Assert.Contains("sats", s.Driver.FindElement(By.Id("AmountDue")).Text);
|
||||
|
||||
// Details should not show exchange rate
|
||||
s.Driver.ToggleCollapse("PaymentDetails");
|
||||
s.Driver.ElementDoesNotExist(By.Id("PaymentDetails-ExchangeRate"));
|
||||
s.Driver.ElementDoesNotExist(By.Id("PaymentDetails-TotalFiat"));
|
||||
s.Driver.ElementDoesNotExist(By.Id("PaymentDetails-RecommendedFee"));
|
||||
Assert.Contains("21 000 sats", s.Driver.FindElement(By.Id("PaymentDetails-AmountDue")).Text);
|
||||
Assert.Contains("21 000 sats", s.Driver.FindElement(By.Id("PaymentDetails-TotalPrice")).Text);
|
||||
|
||||
// Expire
|
||||
var expirySeconds = s.Driver.FindElement(By.Id("ExpirySeconds"));
|
||||
@ -124,7 +140,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.DoesNotContain("Please send", paymentInfo.Text);
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
var expiredSection = s.Driver.FindElement(By.Id("expired"));
|
||||
var expiredSection = s.Driver.FindElement(By.Id("unpaid"));
|
||||
Assert.True(expiredSection.Displayed);
|
||||
Assert.Contains("Invoice Expired", expiredSection.Text);
|
||||
});
|
||||
@ -145,6 +161,10 @@ namespace BTCPayServer.Tests
|
||||
Assert.Contains("Exchange Rate", details.Text);
|
||||
Assert.Contains("Amount Due", details.Text);
|
||||
Assert.Contains("Recommended Fee", details.Text);
|
||||
Assert.Contains("$", s.Driver.FindElement(By.Id("PaymentDetails-ExchangeRate")).Text);
|
||||
Assert.Contains("$", s.Driver.FindElement(By.Id("PaymentDetails-TotalFiat")).Text);
|
||||
Assert.Contains("BTC", s.Driver.FindElement(By.Id("PaymentDetails-AmountDue")).Text);
|
||||
Assert.Contains("BTC", s.Driver.FindElement(By.Id("PaymentDetails-TotalPrice")).Text);
|
||||
|
||||
// Pay partial amount
|
||||
await Task.Delay(200);
|
||||
@ -161,12 +181,27 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
Assert.Contains("Created transaction",
|
||||
s.Driver.WaitForElement(By.Id("CheatSuccessMessage")).Text);
|
||||
s.Server.ExplorerNode.Generate(1);
|
||||
s.Server.ExplorerNode.Generate(2);
|
||||
paymentInfo = s.Driver.WaitForElement(By.Id("PaymentInfo"));
|
||||
Assert.Contains("The invoice hasn't been paid in full", paymentInfo.Text);
|
||||
Assert.Contains("Please send", paymentInfo.Text);
|
||||
});
|
||||
|
||||
// Pay full amount
|
||||
var amountDue = s.Driver.FindElement(By.Id("AmountDue")).GetAttribute("data-amount-due");
|
||||
s.Driver.FindElement(By.Id("FakePayAmount")).FillIn(amountDue);
|
||||
s.Driver.FindElement(By.Id("FakePay")).Click();
|
||||
|
||||
// Processing
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
var processingSection = s.Driver.WaitForElement(By.Id("processing"));
|
||||
Assert.True(processingSection.Displayed);
|
||||
Assert.Contains("Payment Sent", processingSection.Text);
|
||||
Assert.Contains("Your payment has been received and is now processing", processingSection.Text);
|
||||
Assert.True(s.Driver.ElementDoesNotExist(By.Id("confetti")));
|
||||
});
|
||||
|
||||
// Mine
|
||||
s.Driver.FindElement(By.Id("Mine")).Click();
|
||||
TestUtils.Eventually(() =>
|
||||
@ -174,18 +209,15 @@ namespace BTCPayServer.Tests
|
||||
Assert.Contains("Mined 1 block",
|
||||
s.Driver.WaitForElement(By.Id("CheatSuccessMessage")).Text);
|
||||
});
|
||||
|
||||
// Pay full amount
|
||||
var amountDue = s.Driver.FindElement(By.Id("AmountDue")).GetAttribute("data-amount-due");
|
||||
s.Driver.FindElement(By.Id("FakePayAmount")).FillIn(amountDue);
|
||||
s.Driver.FindElement(By.Id("FakePay")).Click();
|
||||
|
||||
// Settled
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
s.Server.ExplorerNode.Generate(1);
|
||||
var paidSection = s.Driver.WaitForElement(By.Id("paid"));
|
||||
Assert.True(paidSection.Displayed);
|
||||
Assert.Contains("Invoice Paid", paidSection.Text);
|
||||
var settledSection = s.Driver.WaitForElement(By.Id("settled"));
|
||||
Assert.True(settledSection.Displayed);
|
||||
Assert.Contains("Invoice Paid", settledSection.Text);
|
||||
});
|
||||
s.Driver.FindElement(By.Id("confetti"));
|
||||
s.Driver.FindElement(By.Id("ReceiptLink"));
|
||||
Assert.Equal(storeUrl, s.Driver.FindElement(By.Id("StoreLink")).GetAttribute("href"));
|
||||
|
||||
@ -193,6 +225,7 @@ namespace BTCPayServer.Tests
|
||||
s.GoToHome();
|
||||
s.GoToStore(StoreNavPages.CheckoutAppearance);
|
||||
s.Driver.SetCheckbox(By.Id("OnChainWithLnInvoiceFallback"), true);
|
||||
s.Driver.SetCheckbox(By.Id("LightningAmountInSatoshi"), false);
|
||||
s.Driver.FindElement(By.Id("Save")).Click();
|
||||
Assert.Contains("Store successfully updated", s.FindAlertMessage().Text);
|
||||
|
||||
@ -200,6 +233,7 @@ namespace BTCPayServer.Tests
|
||||
s.GoToInvoiceCheckout(invoiceId);
|
||||
s.Driver.WaitUntilAvailable(By.Id("Checkout-v2"));
|
||||
Assert.Empty(s.Driver.FindElements(By.CssSelector(".payment-method")));
|
||||
Assert.Contains("BTC", s.Driver.FindElement(By.Id("AmountDue")).Text);
|
||||
qrValue = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-qr-value");
|
||||
address = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
|
||||
payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href");
|
||||
@ -214,7 +248,33 @@ namespace BTCPayServer.Tests
|
||||
Assert.StartsWith($"bitcoin:{address.ToUpperInvariant()}?amount=", qrValue);
|
||||
Assert.Contains("&lightning=LNBCRT", qrValue);
|
||||
s.Driver.FindElement(By.Id("PayByLNURL"));
|
||||
|
||||
|
||||
// Check details
|
||||
s.Driver.ToggleCollapse("PaymentDetails");
|
||||
Assert.Contains("1 BTC = ", s.Driver.FindElement(By.Id("PaymentDetails-ExchangeRate")).Text);
|
||||
Assert.Contains("$", s.Driver.FindElement(By.Id("PaymentDetails-ExchangeRate")).Text);
|
||||
Assert.Contains("$", s.Driver.FindElement(By.Id("PaymentDetails-TotalFiat")).Text);
|
||||
Assert.Contains("BTC", s.Driver.FindElement(By.Id("PaymentDetails-AmountDue")).Text);
|
||||
Assert.Contains("BTC", s.Driver.FindElement(By.Id("PaymentDetails-TotalPrice")).Text);
|
||||
|
||||
// Switch to amount displayed in sats
|
||||
s.GoToHome();
|
||||
s.GoToStore(StoreNavPages.CheckoutAppearance);
|
||||
s.Driver.SetCheckbox(By.Id("LightningAmountInSatoshi"), true);
|
||||
s.Driver.FindElement(By.Id("Save")).Click();
|
||||
Assert.Contains("Store successfully updated", s.FindAlertMessage().Text);
|
||||
s.GoToInvoiceCheckout(invoiceId);
|
||||
s.Driver.WaitUntilAvailable(By.Id("Checkout-v2"));
|
||||
Assert.Contains("sats", s.Driver.FindElement(By.Id("AmountDue")).Text);
|
||||
|
||||
// Check details
|
||||
s.Driver.ToggleCollapse("PaymentDetails");
|
||||
Assert.Contains("1 sat = ", s.Driver.FindElement(By.Id("PaymentDetails-ExchangeRate")).Text);
|
||||
Assert.Contains("$", s.Driver.FindElement(By.Id("PaymentDetails-ExchangeRate")).Text);
|
||||
Assert.Contains("$", s.Driver.FindElement(By.Id("PaymentDetails-TotalFiat")).Text);
|
||||
Assert.Contains("sats", s.Driver.FindElement(By.Id("PaymentDetails-AmountDue")).Text);
|
||||
Assert.Contains("sats", s.Driver.FindElement(By.Id("PaymentDetails-TotalPrice")).Text);
|
||||
|
||||
// BIP21 with LN as default payment method
|
||||
s.GoToHome();
|
||||
invoiceId = s.CreateInvoice(defaultPaymentMethod: "BTC_LightningLike");
|
||||
@ -225,6 +285,14 @@ namespace BTCPayServer.Tests
|
||||
Assert.StartsWith("bitcoin:", payUrl);
|
||||
Assert.Contains("&lightning=lnbcrt", payUrl);
|
||||
s.Driver.FindElement(By.Id("PayByLNURL"));
|
||||
|
||||
// Check details
|
||||
s.Driver.ToggleCollapse("PaymentDetails");
|
||||
Assert.Contains("1 sat = ", s.Driver.FindElement(By.Id("PaymentDetails-ExchangeRate")).Text);
|
||||
Assert.Contains("$", s.Driver.FindElement(By.Id("PaymentDetails-ExchangeRate")).Text);
|
||||
Assert.Contains("$", s.Driver.FindElement(By.Id("PaymentDetails-TotalFiat")).Text);
|
||||
Assert.Contains("sats", s.Driver.FindElement(By.Id("PaymentDetails-AmountDue")).Text);
|
||||
Assert.Contains("sats", s.Driver.FindElement(By.Id("PaymentDetails-TotalPrice")).Text);
|
||||
|
||||
// Ensure LNURL is enabled
|
||||
s.GoToHome();
|
||||
@ -250,6 +318,14 @@ namespace BTCPayServer.Tests
|
||||
Assert.StartsWith("lnurl", copyAddressLightning);
|
||||
Assert.StartsWith($"bitcoin:{address.ToUpperInvariant()}?lightning=LNURL", qrValue);
|
||||
s.Driver.FindElement(By.Id("PayByLNURL"));
|
||||
|
||||
// Check details
|
||||
s.Driver.ToggleCollapse("PaymentDetails");
|
||||
Assert.Contains("1 sat = ", s.Driver.FindElement(By.Id("PaymentDetails-ExchangeRate")).Text);
|
||||
Assert.Contains("$", s.Driver.FindElement(By.Id("PaymentDetails-ExchangeRate")).Text);
|
||||
s.Driver.ElementDoesNotExist(By.Id("PaymentDetails-TotalFiat"));
|
||||
s.Driver.ElementDoesNotExist(By.Id("PaymentDetails-AmountDue"));
|
||||
s.Driver.ElementDoesNotExist(By.Id("PaymentDetails-TotalPrice"));
|
||||
|
||||
// Expiry message should not show amount for top-up invoice
|
||||
expirySeconds = s.Driver.FindElement(By.Id("ExpirySeconds"));
|
||||
|
@ -4,11 +4,11 @@ using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Plugins.Crowdfund;
|
||||
using BTCPayServer.Plugins.Crowdfund.Controllers;
|
||||
using BTCPayServer.Plugins.Crowdfund.Models;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin;
|
||||
using NBitpayClient;
|
||||
@ -34,18 +34,16 @@ namespace BTCPayServer.Tests
|
||||
await user.GrantAccessAsync();
|
||||
var user2 = tester.NewAccount();
|
||||
await user2.GrantAccessAsync();
|
||||
var stores = user.GetController<UIStoresController>();
|
||||
var apps = user.GetController<UIAppsController>();
|
||||
var apps2 = user2.GetController<UIAppsController>();
|
||||
var crowdfund = user.GetController<UICrowdfundController>();
|
||||
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp(user.StoreId)).Model);
|
||||
var appType = AppType.Crowdfund.ToString();
|
||||
Assert.NotNull(vm.SelectedAppType);
|
||||
var appType = CrowdfundAppType.AppType;
|
||||
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp(user.StoreId, appType)).Model);
|
||||
Assert.Equal(appType, vm.SelectedAppType);
|
||||
Assert.Null(vm.AppName);
|
||||
vm.AppName = "test";
|
||||
vm.SelectedAppType = appType;
|
||||
var redirectToAction = Assert.IsType<RedirectToActionResult>(apps.CreateApp(user.StoreId, vm).Result);
|
||||
Assert.Equal(nameof(crowdfund.UpdateCrowdfund), redirectToAction.ActionName);
|
||||
var redirect = Assert.IsType<RedirectResult>(apps.CreateApp(user.StoreId, vm).Result);
|
||||
Assert.EndsWith("/settings/crowdfund", redirect.Url);
|
||||
var appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
|
||||
var app = appList.Apps[0];
|
||||
var appData = new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName, AppType = appType };
|
||||
@ -61,8 +59,8 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal(user.StoreId, appList.Apps[0].StoreId);
|
||||
Assert.IsType<NotFoundResult>(apps2.DeleteApp(appList.Apps[0].Id));
|
||||
Assert.IsType<ViewResult>(apps.DeleteApp(appList.Apps[0].Id));
|
||||
redirectToAction = Assert.IsType<RedirectToActionResult>(apps.DeleteAppPost(appList.Apps[0].Id).Result);
|
||||
Assert.Equal(nameof(stores.Dashboard), redirectToAction.ActionName);
|
||||
var redirectToAction = Assert.IsType<RedirectToActionResult>(apps.DeleteAppPost(appList.Apps[0].Id).Result);
|
||||
Assert.Equal(nameof(UIStoresController.Dashboard), redirectToAction.ActionName);
|
||||
appList = await apps.ListApps(user.StoreId).AssertViewModelAsync<ListAppsViewModel>();
|
||||
Assert.Empty(appList.Apps);
|
||||
}
|
||||
@ -79,10 +77,11 @@ namespace BTCPayServer.Tests
|
||||
var apps = user.GetController<UIAppsController>();
|
||||
var crowdfund = user.GetController<UICrowdfundController>();
|
||||
var vm = apps.CreateApp(user.StoreId).AssertViewModel<CreateAppViewModel>();
|
||||
var appType = AppType.Crowdfund.ToString();
|
||||
var appType = CrowdfundAppType.AppType;
|
||||
vm.AppName = "test";
|
||||
vm.SelectedAppType = appType;
|
||||
Assert.IsType<RedirectToActionResult>(apps.CreateApp(user.StoreId, vm).Result);
|
||||
var redirect = Assert.IsType<RedirectResult>(apps.CreateApp(user.StoreId, vm).Result);
|
||||
Assert.EndsWith("/settings/crowdfund", redirect.Url);
|
||||
var appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
|
||||
var app = appList.Apps[0];
|
||||
var appData = new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName, AppType = appType };
|
||||
@ -105,7 +104,7 @@ namespace BTCPayServer.Tests
|
||||
Amount = new decimal(0.01)
|
||||
}, default));
|
||||
|
||||
Assert.IsType<NotFoundResult>(await anonAppPubsController.ViewCrowdfund(app.Id, string.Empty));
|
||||
Assert.IsType<NotFoundResult>(await anonAppPubsController.ViewCrowdfund(app.Id));
|
||||
|
||||
//Scenario 2: Not Enabled But Admin - Allowed
|
||||
Assert.IsType<OkObjectResult>(await crowdfundController.ContributeToCrowdfund(app.Id, new ContributeToCrowdfund()
|
||||
@ -113,8 +112,8 @@ namespace BTCPayServer.Tests
|
||||
RedirectToCheckout = false,
|
||||
Amount = new decimal(0.01)
|
||||
}, default));
|
||||
Assert.IsType<ViewResult>(await crowdfundController.ViewCrowdfund(app.Id, string.Empty));
|
||||
Assert.IsType<NotFoundResult>(await anonAppPubsController.ViewCrowdfund(app.Id, string.Empty));
|
||||
Assert.IsType<ViewResult>(await crowdfundController.ViewCrowdfund(app.Id));
|
||||
Assert.IsType<NotFoundResult>(await anonAppPubsController.ViewCrowdfund(app.Id));
|
||||
|
||||
//Scenario 3: Enabled But Start Date > Now - Not Allowed
|
||||
crowdfundViewModel.StartDate = DateTime.Today.AddDays(2);
|
||||
@ -170,10 +169,10 @@ namespace BTCPayServer.Tests
|
||||
var apps = user.GetController<UIAppsController>();
|
||||
var crowdfund = user.GetController<UICrowdfundController>();
|
||||
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp(user.StoreId)).Model);
|
||||
var appType = AppType.Crowdfund.ToString();
|
||||
var appType = CrowdfundAppType.AppType;
|
||||
vm.AppName = "test";
|
||||
vm.SelectedAppType = appType;
|
||||
Assert.IsType<RedirectToActionResult>(apps.CreateApp(user.StoreId, vm).Result);
|
||||
Assert.IsType<RedirectResult>(apps.CreateApp(user.StoreId, vm).Result);
|
||||
var appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
|
||||
var app = appList.Apps[0];
|
||||
var appData = new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName, AppType = appType };
|
||||
@ -193,7 +192,7 @@ namespace BTCPayServer.Tests
|
||||
var publicApps = user.GetController<UICrowdfundController>();
|
||||
|
||||
var model = Assert.IsType<ViewCrowdfundViewModel>(Assert
|
||||
.IsType<ViewResult>(publicApps.ViewCrowdfund(app.Id, String.Empty).Result).Model);
|
||||
.IsType<ViewResult>(publicApps.ViewCrowdfund(app.Id).Result).Model);
|
||||
|
||||
Assert.Equal(crowdfundViewModel.TargetAmount, model.TargetAmount);
|
||||
Assert.Equal(crowdfundViewModel.EndDate, model.EndDate);
|
||||
@ -217,7 +216,7 @@ namespace BTCPayServer.Tests
|
||||
}, Facade.Merchant);
|
||||
|
||||
model = Assert.IsType<ViewCrowdfundViewModel>(Assert
|
||||
.IsType<ViewResult>(publicApps.ViewCrowdfund(app.Id, string.Empty).Result).Model);
|
||||
.IsType<ViewResult>(publicApps.ViewCrowdfund(app.Id).Result).Model);
|
||||
|
||||
Assert.Equal(0m, model.Info.CurrentAmount);
|
||||
Assert.Equal(1m, model.Info.CurrentPendingAmount);
|
||||
@ -226,12 +225,12 @@ namespace BTCPayServer.Tests
|
||||
|
||||
TestLogs.LogInformation("Let's check current amount change once payment is confirmed");
|
||||
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, tester.ExplorerNode.Network);
|
||||
tester.ExplorerNode.SendToAddress(invoiceAddress, invoice.BtcDue);
|
||||
tester.ExplorerNode.Generate(1); // By default invoice confirmed at 1 block
|
||||
await tester.ExplorerNode.SendToAddressAsync(invoiceAddress, invoice.BtcDue);
|
||||
await tester.ExplorerNode.GenerateAsync(1); // By default invoice confirmed at 1 block
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
model = Assert.IsType<ViewCrowdfundViewModel>(Assert
|
||||
.IsType<ViewResult>(publicApps.ViewCrowdfund(app.Id, String.Empty).Result).Model);
|
||||
.IsType<ViewResult>(publicApps.ViewCrowdfund(app.Id).Result).Model);
|
||||
Assert.Equal(1m, model.Info.CurrentAmount);
|
||||
Assert.Equal(0m, model.Info.CurrentPendingAmount);
|
||||
});
|
||||
@ -279,7 +278,7 @@ namespace BTCPayServer.Tests
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
model = Assert.IsType<ViewCrowdfundViewModel>(Assert
|
||||
.IsType<ViewResult>(publicApps.ViewCrowdfund(app.Id, string.Empty).Result).Model);
|
||||
.IsType<ViewResult>(publicApps.ViewCrowdfund(app.Id).Result).Model);
|
||||
Assert.Equal(0.7m, model.Info.CurrentPendingAmount);
|
||||
});
|
||||
}
|
||||
|
@ -51,7 +51,6 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
public FastTests(ITestOutputHelper helper) : base(helper)
|
||||
{
|
||||
|
||||
}
|
||||
class DockerImage
|
||||
{
|
||||
@ -326,7 +325,7 @@ namespace BTCPayServer.Tests
|
||||
var networkProvider = new BTCPayNetworkProvider(ChainName.Regtest);
|
||||
var paymentMethodHandlerDictionary = new PaymentMethodHandlerDictionary(new IPaymentMethodHandler[]
|
||||
{
|
||||
new BitcoinLikePaymentHandler(null, networkProvider, null, null, null),
|
||||
new BitcoinLikePaymentHandler(null, networkProvider, null, null, null, null),
|
||||
new LightningLikePaymentHandler(null, null, networkProvider, null, null, null),
|
||||
});
|
||||
var entity = new InvoiceEntity();
|
||||
@ -512,7 +511,7 @@ namespace BTCPayServer.Tests
|
||||
var networkProvider = new BTCPayNetworkProvider(ChainName.Regtest);
|
||||
var paymentMethodHandlerDictionary = new PaymentMethodHandlerDictionary(new IPaymentMethodHandler[]
|
||||
{
|
||||
new BitcoinLikePaymentHandler(null, networkProvider, null, null, null),
|
||||
new BitcoinLikePaymentHandler(null, networkProvider, null, null, null, null),
|
||||
new LightningLikePaymentHandler(null, null, networkProvider, null, null, null),
|
||||
});
|
||||
var entity = new InvoiceEntity();
|
||||
@ -600,15 +599,16 @@ namespace BTCPayServer.Tests
|
||||
[Fact]
|
||||
public void RoundupCurrenciesCorrectly()
|
||||
{
|
||||
DisplayFormatter displayFormatter = new (CurrencyNameTable.Instance);
|
||||
foreach (var test in new[]
|
||||
{
|
||||
(0.0005m, "$0.0005 (USD)", "USD"), (0.001m, "$0.001 (USD)", "USD"), (0.01m, "$0.01 (USD)", "USD"),
|
||||
(0.1m, "$0.10 (USD)", "USD"), (0.1m, "0,10 € (EUR)", "EUR"), (1000m, "¥1,000 (JPY)", "JPY"),
|
||||
(1000.0001m, "₹ 1,000.00 (INR)", "INR"),
|
||||
(0.0m, "$0.00 (USD)", "USD")
|
||||
(0.0005m, "0.0005 USD", "USD"), (0.001m, "0.001 USD", "USD"), (0.01m, "0.01 USD", "USD"),
|
||||
(0.1m, "0.10 USD", "USD"), (0.1m, "0,10 EUR", "EUR"), (1000m, "1,000 JPY", "JPY"),
|
||||
(1000.0001m, "1,000.00 INR", "INR"),
|
||||
(0.0m, "0.00 USD", "USD")
|
||||
})
|
||||
{
|
||||
var actual = CurrencyNameTable.Instance.DisplayFormatCurrency(test.Item1, test.Item3);
|
||||
var actual = displayFormatter.Currency(test.Item1, test.Item3);
|
||||
actual = actual.Replace("¥", "¥"); // Hack so JPY test pass on linux as well
|
||||
Assert.Equal(test.Item2, actual);
|
||||
}
|
||||
@ -706,22 +706,69 @@ namespace BTCPayServer.Tests
|
||||
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 "));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseDerivationSchemeSettings()
|
||||
{
|
||||
var testnet = new BTCPayNetworkProvider(ChainName.Testnet).GetNetwork<BTCPayNetwork>("BTC");
|
||||
var mainnet = new BTCPayNetworkProvider(ChainName.Mainnet).GetNetwork<BTCPayNetwork>("BTC");
|
||||
var root = new Mnemonic(
|
||||
"usage fever hen zero slide mammal silent heavy donate budget pulse say brain thank sausage brand craft about save attract muffin advance illegal cabbage")
|
||||
.DeriveExtKey();
|
||||
|
||||
// xpub
|
||||
var tpub = "tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS";
|
||||
Assert.True(DerivationSchemeSettings.TryParseFromWalletFile(tpub, testnet, out var settings, out var error));
|
||||
Assert.Null(error);
|
||||
Assert.True(settings.AccountDerivation is DirectDerivationStrategy { Segwit: false });
|
||||
Assert.Equal($"{tpub}-[legacy]", ((DirectDerivationStrategy)settings.AccountDerivation).ToString());
|
||||
|
||||
// xpub with fingerprint and account
|
||||
tpub = "tpubDCXK98mNrPWuoWweaoUkqwxQF5NMWpQLy7n7XJgDCpwYfoZRXGafPaVM7mYqD7UKhsbMxkN864JY2PniMkt1Uk4dNuAMnWFVqdquyvZNyca";
|
||||
var vpub = "vpub5YVA1ZbrqkUVq8NZTtvRDrS2a1yoeBvHbG9NbxqJ6uRtpKGFwjQT11WEqKYsgoDF6gpqrDf8ddmPZe4yXWCjzqF8ad2Cw9xHiE8DSi3X3ik";
|
||||
var fingerprint = "e5746fd9";
|
||||
var account = "84'/1'/0'";
|
||||
var str = $"[{fingerprint}/{account}]{vpub}";
|
||||
Assert.True(DerivationSchemeSettings.TryParseFromWalletFile(str, testnet, out settings, out error));
|
||||
Assert.Null(error);
|
||||
Assert.True(settings.AccountDerivation is DirectDerivationStrategy { Segwit: true });
|
||||
Assert.Equal(vpub, settings.AccountOriginal);
|
||||
Assert.Equal(tpub, ((DirectDerivationStrategy)settings.AccountDerivation).ToString());
|
||||
Assert.Equal(HDFingerprint.TryParse(fingerprint, out var hd) ? hd : default, settings.AccountKeySettings[0].RootFingerprint);
|
||||
Assert.Equal(account, settings.AccountKeySettings[0].AccountKeyPath.ToString());
|
||||
|
||||
// ColdCard
|
||||
Assert.True(DerivationSchemeSettings.TryParseFromWalletFile(
|
||||
"{\"keystore\": {\"ckcc_xpub\": \"xpub661MyMwAqRbcGVBsTGeNZN6QGVHmMHLdSA4FteGsRrEriu4pnVZMZWnruFFFXkMnyoBjyHndD3Qwcfz4MPzBUxjSevweNFQx7SAYZATtcDw\", \"xpub\": \"ypub6WWc2gWwHbdnAAyJDnR4SPL1phRh7REqrPBfZeizaQ1EmTshieRXJC3Z5YoU4wkcdKHEjQGkh6AYEzCQC1Kz3DNaWSwdc1pc8416hAjzqyD\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/49'/0'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}",
|
||||
mainnet, out var settings, out var error));
|
||||
mainnet, out settings, out error));
|
||||
Assert.Null(error);
|
||||
Assert.Equal(root.GetPublicKey().GetHDFingerPrint(), settings.AccountKeySettings[0].RootFingerprint);
|
||||
Assert.Equal(settings.AccountKeySettings[0].RootFingerprint,
|
||||
HDFingerprint.TryParse("8bafd160", out var hd) ? hd : default);
|
||||
HDFingerprint.TryParse("8bafd160", out hd) ? hd : default);
|
||||
Assert.Equal("Coldcard Import 0x60d1af8b", settings.Label);
|
||||
Assert.Equal("49'/0'/0'", settings.AccountKeySettings[0].AccountKeyPath.ToString());
|
||||
Assert.Equal(
|
||||
@ -729,28 +776,26 @@ namespace BTCPayServer.Tests
|
||||
settings.AccountOriginal);
|
||||
Assert.Equal(root.Derive(new KeyPath("m/49'/0'/0'")).Neuter().PubKey.WitHash.ScriptPubKey.Hash.ScriptPubKey,
|
||||
settings.AccountDerivation.GetDerivation().ScriptPubKey);
|
||||
var testnet = new BTCPayNetworkProvider(ChainName.Testnet).GetNetwork<BTCPayNetwork>("BTC");
|
||||
|
||||
// Should be legacy
|
||||
Assert.True(DerivationSchemeSettings.TryParseFromWalletFile(
|
||||
"{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"tpubDDWYqT3P24znfsaGX7kZcQhNc5LAjnQiKQvUCHF2jS6dsgJBRtymopEU5uGpMaR5YChjuiExZG1X2aTbqXkp82KqH5qnqwWHp6EWis9ZvKr\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/44'/1'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}",
|
||||
testnet, out settings, out error));
|
||||
Assert.True(settings.AccountDerivation is DirectDerivationStrategy s && !s.Segwit);
|
||||
Assert.True(settings.AccountDerivation is DirectDerivationStrategy { Segwit: false });
|
||||
Assert.Null(error);
|
||||
|
||||
// Should be segwit p2sh
|
||||
Assert.True(DerivationSchemeSettings.TryParseFromWalletFile(
|
||||
"{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"upub5DSddA9NoRUyJrQ4p86nsCiTSY7kLHrSxx3joEJXjHd4HPARhdXUATuk585FdWPVC2GdjsMePHb6BMDmf7c6KG4K4RPX6LVqBLtDcWpQJmh\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/49'/1'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}",
|
||||
testnet, out settings, out error));
|
||||
Assert.True(settings.AccountDerivation is P2SHDerivationStrategy p &&
|
||||
p.Inner is DirectDerivationStrategy s2 && s2.Segwit);
|
||||
Assert.True(settings.AccountDerivation is P2SHDerivationStrategy { Inner: DirectDerivationStrategy { Segwit: true } });
|
||||
Assert.Null(error);
|
||||
|
||||
// Should be segwit
|
||||
Assert.True(DerivationSchemeSettings.TryParseFromWalletFile(
|
||||
"{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"vpub5YjYxTemJ39tFRnuAhwduyxG2tKGjoEpmvqVQRPqdYrqa6YGoeSzBtHXaJUYB19zDbXs3JjbEcVWERjQBPf9bEfUUMZNMv1QnMyHV8JPqyf\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/84'/1'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}",
|
||||
testnet, out settings, out error));
|
||||
Assert.True(settings.AccountDerivation is DirectDerivationStrategy s3 && s3.Segwit);
|
||||
Assert.True(settings.AccountDerivation is DirectDerivationStrategy { Segwit: true });
|
||||
Assert.Null(error);
|
||||
|
||||
// Specter
|
||||
@ -1466,14 +1511,14 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal(1m / 0.000061m, rule2.BidAsk.Bid);
|
||||
|
||||
// testing rounding
|
||||
rule2 = rules.GetRuleFor(CurrencyPair.Parse("Sats_EUR"));
|
||||
rule2 = rules.GetRuleFor(CurrencyPair.Parse("SATS_EUR"));
|
||||
rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_EUR"), new BidAsk(1.23m, 2.34m));
|
||||
Assert.True(rule2.Reevaluate());
|
||||
Assert.Equal("0.00000001 * (1.23, 2.34)", rule2.ToString(true));
|
||||
Assert.Equal(0.0000000234m, rule2.BidAsk.Ask);
|
||||
Assert.Equal(0.0000000123m, rule2.BidAsk.Bid);
|
||||
|
||||
rule2 = rules.GetRuleFor(CurrencyPair.Parse("EUR_Sats"));
|
||||
rule2 = rules.GetRuleFor(CurrencyPair.Parse("EUR_SATS"));
|
||||
rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_EUR"), new BidAsk(1.23m, 2.34m));
|
||||
Assert.True(rule2.Reevaluate());
|
||||
Assert.Equal("1 / (0.00000001 * (1.23, 2.34))", rule2.ToString(true));
|
||||
@ -1715,7 +1760,7 @@ namespace BTCPayServer.Tests
|
||||
var networkProvider = new BTCPayNetworkProvider(ChainName.Regtest);
|
||||
var paymentMethodHandlerDictionary = new PaymentMethodHandlerDictionary(new IPaymentMethodHandler[]
|
||||
{
|
||||
new BitcoinLikePaymentHandler(null, networkProvider, null, null, null),
|
||||
new BitcoinLikePaymentHandler(null, networkProvider, null, null, null, null),
|
||||
new LightningLikePaymentHandler(null, null, networkProvider, null, null, null),
|
||||
});
|
||||
var networkBTC = networkProvider.GetNetwork("BTC");
|
||||
|
@ -3940,8 +3940,7 @@ clientBasic.PreviewUpdateStoreRateConfiguration(user.StoreId, new StoreRateConfi
|
||||
var withdrawalClient = await admin.CreateClient(Policies.CanWithdrawFromCustodianAccounts);
|
||||
var depositClient = await admin.CreateClient(Policies.CanDepositToCustodianAccounts);
|
||||
var tradeClient = await admin.CreateClient(Policies.CanTradeCustodianAccount);
|
||||
|
||||
|
||||
|
||||
var store = await adminClient.GetStore(admin.StoreId);
|
||||
var storeId = store.Id;
|
||||
|
||||
@ -3981,22 +3980,22 @@ clientBasic.PreviewUpdateStoreRateConfiguration(user.StoreId, new StoreRateConfi
|
||||
|
||||
|
||||
// Test: GetDepositAddress, unauth
|
||||
await AssertHttpError(401, async () => await unauthClient.GetDepositAddress(storeId, accountId, MockCustodian.DepositPaymentMethod));
|
||||
|
||||
await AssertHttpError(401, async () => await unauthClient.GetCustodianAccountDepositAddress(storeId, accountId, MockCustodian.DepositPaymentMethod));
|
||||
|
||||
// Test: GetDepositAddress, auth, but wrong permission
|
||||
await AssertHttpError(403, async () => await managerClient.GetDepositAddress(storeId, accountId, MockCustodian.DepositPaymentMethod));
|
||||
|
||||
await AssertHttpError(403, async () => await managerClient.GetCustodianAccountDepositAddress(storeId, accountId, MockCustodian.DepositPaymentMethod));
|
||||
|
||||
// Test: GetDepositAddress, wrong payment method
|
||||
await AssertHttpError(400, async () => await depositClient.GetDepositAddress(storeId, accountId, "WRONG-PaymentMethod"));
|
||||
|
||||
await AssertApiError( 400, "unsupported-payment-method", async () => await depositClient.GetCustodianAccountDepositAddress(storeId, accountId, "WRONG-PaymentMethod"));
|
||||
|
||||
// Test: GetDepositAddress, wrong store ID
|
||||
await AssertHttpError(403, async () => await depositClient.GetDepositAddress("WRONG-STORE", accountId, MockCustodian.DepositPaymentMethod));
|
||||
|
||||
await AssertHttpError(403, async () => await depositClient.GetCustodianAccountDepositAddress("WRONG-STORE", accountId, MockCustodian.DepositPaymentMethod));
|
||||
|
||||
// Test: GetDepositAddress, wrong account ID
|
||||
await AssertHttpError(404, async () => await depositClient.GetDepositAddress(storeId, "WRONG-ACCOUNT-ID", MockCustodian.DepositPaymentMethod));
|
||||
|
||||
await AssertHttpError(404, async () => await depositClient.GetCustodianAccountDepositAddress(storeId, "WRONG-ACCOUNT-ID", MockCustodian.DepositPaymentMethod));
|
||||
|
||||
// Test: GetDepositAddress, correct payment method
|
||||
var depositAddress = await depositClient.GetDepositAddress(storeId, accountId, MockCustodian.DepositPaymentMethod);
|
||||
var depositAddress = await depositClient.GetCustodianAccountDepositAddress(storeId, accountId, MockCustodian.DepositPaymentMethod);
|
||||
Assert.NotNull(depositAddress);
|
||||
Assert.Equal(MockCustodian.DepositAddress, depositAddress.Address);
|
||||
|
||||
@ -4054,13 +4053,13 @@ clientBasic.PreviewUpdateStoreRateConfiguration(user.StoreId, new StoreRateConfi
|
||||
|
||||
|
||||
// Test: GetTradeQuote, unauth
|
||||
await AssertHttpError(401, async () => await unauthClient.GetTradeQuote(storeId, accountId, MockCustodian.TradeFromAsset, MockCustodian.TradeToAsset));
|
||||
|
||||
await AssertHttpError(401, async () => await unauthClient.GetCustodianAccountTradeQuote(storeId, accountId, MockCustodian.TradeFromAsset, MockCustodian.TradeToAsset));
|
||||
|
||||
// Test: GetTradeQuote, auth, but wrong permission
|
||||
await AssertHttpError(403, async () => await managerClient.GetTradeQuote(storeId, accountId, MockCustodian.TradeFromAsset, MockCustodian.TradeToAsset));
|
||||
|
||||
await AssertHttpError(403, async () => await managerClient.GetCustodianAccountTradeQuote(storeId, accountId, MockCustodian.TradeFromAsset, MockCustodian.TradeToAsset));
|
||||
|
||||
// Test: GetTradeQuote, auth, correct permission
|
||||
var tradeQuote = await tradeClient.GetTradeQuote(storeId, accountId, MockCustodian.TradeFromAsset, MockCustodian.TradeToAsset);
|
||||
var tradeQuote = await tradeClient.GetCustodianAccountTradeQuote(storeId, accountId, MockCustodian.TradeFromAsset, MockCustodian.TradeToAsset);
|
||||
Assert.NotNull(tradeQuote);
|
||||
Assert.Equal(MockCustodian.TradeFromAsset, tradeQuote.FromAsset);
|
||||
Assert.Equal(MockCustodian.TradeToAsset, tradeQuote.ToAsset);
|
||||
@ -4068,30 +4067,30 @@ clientBasic.PreviewUpdateStoreRateConfiguration(user.StoreId, new StoreRateConfi
|
||||
Assert.Equal(MockCustodian.BtcPriceInEuro, tradeQuote.Ask);
|
||||
|
||||
// Test: GetTradeQuote, SATS
|
||||
await AssertApiError(400, "use-asset-synonym", async () => await tradeClient.GetTradeQuote(storeId, accountId, MockCustodian.TradeFromAsset, "SATS"));
|
||||
|
||||
await AssertApiError(400, "use-asset-synonym", async () => await tradeClient.GetCustodianAccountTradeQuote(storeId, accountId, MockCustodian.TradeFromAsset, "SATS"));
|
||||
|
||||
// Test: GetTradeQuote, wrong asset
|
||||
await AssertHttpError(404, async () => await tradeClient.GetTradeQuote(storeId, accountId, "WRONG-ASSET", MockCustodian.TradeToAsset));
|
||||
await AssertHttpError(404, async () => await tradeClient.GetTradeQuote(storeId, accountId, MockCustodian.TradeFromAsset, "WRONG-ASSET"));
|
||||
await AssertHttpError(404, async () => await tradeClient.GetCustodianAccountTradeQuote(storeId, accountId, "WRONG-ASSET", MockCustodian.TradeToAsset));
|
||||
await AssertHttpError(404, async () => await tradeClient.GetCustodianAccountTradeQuote(storeId, accountId, MockCustodian.TradeFromAsset , "WRONG-ASSET"));
|
||||
|
||||
// Test: wrong account ID
|
||||
await AssertHttpError(404, async () => await tradeClient.GetTradeQuote(storeId, "WRONG-ACCOUNT-ID", MockCustodian.TradeFromAsset, MockCustodian.TradeToAsset));
|
||||
|
||||
await AssertHttpError(404, async () => await tradeClient.GetCustodianAccountTradeQuote(storeId, "WRONG-ACCOUNT-ID", MockCustodian.TradeFromAsset, MockCustodian.TradeToAsset));
|
||||
|
||||
// Test: wrong store ID
|
||||
await AssertHttpError(403, async () => await tradeClient.GetTradeQuote("WRONG-STORE-ID", accountId, MockCustodian.TradeFromAsset, MockCustodian.TradeToAsset));
|
||||
await AssertHttpError(403, async () => await tradeClient.GetCustodianAccountTradeQuote("WRONG-STORE-ID", accountId, MockCustodian.TradeFromAsset, MockCustodian.TradeToAsset));
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// Test: GetTradeInfo, unauth
|
||||
await AssertHttpError(401, async () => await unauthClient.GetTradeInfo(storeId, accountId, MockCustodian.TradeId));
|
||||
|
||||
await AssertHttpError(401, async () => await unauthClient.GetCustodianAccountTradeInfo(storeId, accountId, MockCustodian.TradeId));
|
||||
|
||||
// Test: GetTradeInfo, auth, but wrong permission
|
||||
await AssertHttpError(403, async () => await managerClient.GetTradeInfo(storeId, accountId, MockCustodian.TradeId));
|
||||
|
||||
await AssertHttpError(403, async () => await managerClient.GetCustodianAccountTradeInfo(storeId, accountId, MockCustodian.TradeId));
|
||||
|
||||
// Test: GetTradeInfo, auth, correct permission
|
||||
var tradeResult = await tradeClient.GetTradeInfo(storeId, accountId, MockCustodian.TradeId);
|
||||
var tradeResult = await tradeClient.GetCustodianAccountTradeInfo(storeId, accountId, MockCustodian.TradeId);
|
||||
Assert.NotNull(tradeResult);
|
||||
Assert.Equal(accountId, tradeResult.AccountId);
|
||||
Assert.Equal(mockCustodian.Code, tradeResult.CustodianCode);
|
||||
@ -4111,66 +4110,93 @@ clientBasic.PreviewUpdateStoreRateConfiguration(user.StoreId, new StoreRateConfi
|
||||
Assert.Equal(LedgerEntryData.LedgerEntryType.Fee, tradeResult.LedgerEntries[2].Type);
|
||||
|
||||
// Test: GetTradeInfo, wrong trade ID
|
||||
await AssertHttpError(404, async () => await tradeClient.GetTradeInfo(storeId, accountId, "WRONG-TRADE-ID"));
|
||||
|
||||
await AssertHttpError(404, async () => await tradeClient.GetCustodianAccountTradeInfo(storeId, accountId, "WRONG-TRADE-ID"));
|
||||
|
||||
// Test: wrong account ID
|
||||
await AssertHttpError(404, async () => await tradeClient.GetTradeInfo(storeId, "WRONG-ACCOUNT-ID", MockCustodian.TradeId));
|
||||
|
||||
await AssertHttpError(404, async () => await tradeClient.GetCustodianAccountTradeInfo(storeId, "WRONG-ACCOUNT-ID", MockCustodian.TradeId));
|
||||
|
||||
// Test: wrong store ID
|
||||
await AssertHttpError(403, async () => await tradeClient.GetTradeInfo("WRONG-STORE-ID", accountId, MockCustodian.TradeId));
|
||||
await AssertHttpError(403, async () => await tradeClient.GetCustodianAccountTradeInfo("WRONG-STORE-ID", accountId, MockCustodian.TradeId));
|
||||
|
||||
var qty = new TradeQuantity(MockCustodian.WithdrawalAmount, TradeQuantity.ValueType.Exact);
|
||||
// Test: SimulateWithdrawal, unauth
|
||||
var simulateWithdrawalRequest = new WithdrawRequestData(MockCustodian.WithdrawalPaymentMethod, qty);
|
||||
await AssertHttpError(401, async () => await unauthClient.SimulateCustodianAccountWithdrawal(storeId, accountId, simulateWithdrawalRequest));
|
||||
|
||||
// Test: SimulateWithdrawal, auth, but wrong permission
|
||||
await AssertHttpError(403, async () => await managerClient.SimulateCustodianAccountWithdrawal(storeId, accountId, simulateWithdrawalRequest));
|
||||
|
||||
// Test: SimulateWithdrawal, correct payment method, correct amount
|
||||
var simulateWithdrawResponse = await withdrawalClient.SimulateCustodianAccountWithdrawal(storeId, accountId, simulateWithdrawalRequest);
|
||||
AssertMockWithdrawal(simulateWithdrawResponse, custodianAccountData);
|
||||
|
||||
// Test: SimulateWithdrawal, wrong payment method
|
||||
var wrongPaymentMethodSimulateWithdrawalRequest = new WithdrawRequestData("WRONG-PAYMENT-METHOD", qty);
|
||||
await AssertApiError( 400, "unsupported-payment-method", async () => await withdrawalClient.SimulateCustodianAccountWithdrawal(storeId, accountId, wrongPaymentMethodSimulateWithdrawalRequest));
|
||||
|
||||
// Test: SimulateWithdrawal, wrong account ID
|
||||
await AssertHttpError(404, async () => await withdrawalClient.SimulateCustodianAccountWithdrawal(storeId, "WRONG-ACCOUNT-ID", simulateWithdrawalRequest));
|
||||
|
||||
// Test: SimulateWithdrawal, wrong store ID
|
||||
// TODO it is wierd that 403 is considered normal, but it is like this for all calls where the store is wrong... I'd have preferred a 404 error, because the store cannot be found.
|
||||
await AssertHttpError(403, async () => await withdrawalClient.SimulateCustodianAccountWithdrawal( "WRONG-STORE-ID",accountId, simulateWithdrawalRequest));
|
||||
|
||||
// Test: SimulateWithdrawal, correct payment method, wrong amount
|
||||
var wrongAmountSimulateWithdrawalRequest = new WithdrawRequestData(MockCustodian.WithdrawalPaymentMethod, TradeQuantity.Parse("0.666"));
|
||||
await AssertHttpError(400, async () => await withdrawalClient.SimulateCustodianAccountWithdrawal(storeId, accountId, wrongAmountSimulateWithdrawalRequest));
|
||||
|
||||
// Test: CreateWithdrawal, unauth
|
||||
var createWithdrawalRequest = new WithdrawRequestData(MockCustodian.WithdrawalPaymentMethod, MockCustodian.WithdrawalAmount);
|
||||
await AssertHttpError(401, async () => await unauthClient.CreateWithdrawal(storeId, accountId, createWithdrawalRequest));
|
||||
|
||||
var createWithdrawalRequest = new WithdrawRequestData(MockCustodian.WithdrawalPaymentMethod, qty);
|
||||
var createWithdrawalRequestPercentage = new WithdrawRequestData(MockCustodian.WithdrawalPaymentMethod, qty);
|
||||
await AssertHttpError(401, async () => await unauthClient.CreateCustodianAccountWithdrawal(storeId, accountId, createWithdrawalRequest));
|
||||
|
||||
// Test: CreateWithdrawal, auth, but wrong permission
|
||||
await AssertHttpError(403, async () => await managerClient.CreateWithdrawal(storeId, accountId, createWithdrawalRequest));
|
||||
|
||||
await AssertHttpError(403, async () => await managerClient.CreateCustodianAccountWithdrawal(storeId, accountId, createWithdrawalRequest));
|
||||
|
||||
// Test: CreateWithdrawal, correct payment method, correct amount
|
||||
var withdrawResponse = await withdrawalClient.CreateWithdrawal(storeId, accountId, createWithdrawalRequest);
|
||||
var withdrawResponse = await withdrawalClient.CreateCustodianAccountWithdrawal(storeId, accountId, createWithdrawalRequest);
|
||||
AssertMockWithdrawal(withdrawResponse, custodianAccountData);
|
||||
|
||||
|
||||
|
||||
// Test: CreateWithdrawal, correct payment method, correct amount, but as a percentage
|
||||
var withdrawWithPercentageResponse = await withdrawalClient.CreateCustodianAccountWithdrawal(storeId, accountId, createWithdrawalRequestPercentage);
|
||||
AssertMockWithdrawal(withdrawWithPercentageResponse, custodianAccountData);
|
||||
|
||||
// Test: CreateWithdrawal, wrong payment method
|
||||
var wrongPaymentMethodCreateWithdrawalRequest = new WithdrawRequestData("WRONG-PAYMENT-METHOD", MockCustodian.WithdrawalAmount);
|
||||
await AssertHttpError(403, async () => await withdrawalClient.CreateWithdrawal(storeId, accountId, wrongPaymentMethodCreateWithdrawalRequest));
|
||||
|
||||
var wrongPaymentMethodCreateWithdrawalRequest = new WithdrawRequestData("WRONG-PAYMENT-METHOD", qty);
|
||||
await AssertApiError( 400, "unsupported-payment-method", async () => await withdrawalClient.CreateCustodianAccountWithdrawal(storeId, accountId, wrongPaymentMethodCreateWithdrawalRequest));
|
||||
|
||||
// Test: CreateWithdrawal, wrong account ID
|
||||
await AssertHttpError(404, async () => await withdrawalClient.CreateWithdrawal(storeId, "WRONG-ACCOUNT-ID", createWithdrawalRequest));
|
||||
|
||||
await AssertHttpError(404, async () => await withdrawalClient.CreateCustodianAccountWithdrawal(storeId, "WRONG-ACCOUNT-ID", createWithdrawalRequest));
|
||||
|
||||
// Test: CreateWithdrawal, wrong store ID
|
||||
// TODO it is wierd that 403 is considered normal, but it is like this for all calls where the store is wrong... I'd have preferred a 404 error, because the store cannot be found.
|
||||
await AssertHttpError(403, async () => await withdrawalClient.CreateWithdrawal("WRONG-STORE-ID", accountId, createWithdrawalRequest));
|
||||
|
||||
await AssertHttpError(403, async () => await withdrawalClient.CreateCustodianAccountWithdrawal( "WRONG-STORE-ID",accountId, createWithdrawalRequest));
|
||||
|
||||
// Test: CreateWithdrawal, correct payment method, wrong amount
|
||||
var wrongAmountCreateWithdrawalRequest = new WithdrawRequestData(MockCustodian.WithdrawalPaymentMethod, new decimal(0.666));
|
||||
await AssertHttpError(400, async () => await withdrawalClient.CreateWithdrawal(storeId, accountId, wrongAmountCreateWithdrawalRequest));
|
||||
|
||||
var wrongAmountCreateWithdrawalRequest = new WithdrawRequestData(MockCustodian.WithdrawalPaymentMethod, TradeQuantity.Parse("0.666"));
|
||||
await AssertHttpError(400, async () => await withdrawalClient.CreateCustodianAccountWithdrawal(storeId, accountId, wrongAmountCreateWithdrawalRequest));
|
||||
|
||||
// Test: GetWithdrawalInfo, unauth
|
||||
await AssertHttpError(401, async () => await unauthClient.GetWithdrawalInfo(storeId, accountId, MockCustodian.WithdrawalPaymentMethod, MockCustodian.WithdrawalId));
|
||||
|
||||
await AssertHttpError(401, async () => await unauthClient.GetCustodianAccountWithdrawalInfo(storeId, accountId, MockCustodian.WithdrawalPaymentMethod, MockCustodian.WithdrawalId));
|
||||
|
||||
// Test: GetWithdrawalInfo, auth, but wrong permission
|
||||
await AssertHttpError(403, async () => await managerClient.GetWithdrawalInfo(storeId, accountId, MockCustodian.WithdrawalPaymentMethod, MockCustodian.WithdrawalId));
|
||||
|
||||
await AssertHttpError(403, async () => await managerClient.GetCustodianAccountWithdrawalInfo(storeId, accountId, MockCustodian.WithdrawalPaymentMethod, MockCustodian.WithdrawalId));
|
||||
|
||||
// Test: GetWithdrawalInfo, auth, correct permission
|
||||
var withdrawalInfo = await withdrawalClient.GetWithdrawalInfo(storeId, accountId, MockCustodian.WithdrawalPaymentMethod, MockCustodian.WithdrawalId);
|
||||
var withdrawalInfo = await withdrawalClient.GetCustodianAccountWithdrawalInfo(storeId, accountId, MockCustodian.WithdrawalPaymentMethod, MockCustodian.WithdrawalId);
|
||||
AssertMockWithdrawal(withdrawalInfo, custodianAccountData);
|
||||
|
||||
// Test: GetWithdrawalInfo, wrong withdrawal ID
|
||||
await AssertHttpError(404, async () => await withdrawalClient.GetWithdrawalInfo(storeId, accountId, MockCustodian.WithdrawalPaymentMethod, "WRONG-WITHDRAWAL-ID"));
|
||||
|
||||
await AssertHttpError(404, async () => await withdrawalClient.GetCustodianAccountWithdrawalInfo(storeId, accountId, MockCustodian.WithdrawalPaymentMethod, "WRONG-WITHDRAWAL-ID"));
|
||||
|
||||
// Test: wrong account ID
|
||||
await AssertHttpError(404, async () => await withdrawalClient.GetWithdrawalInfo(storeId, "WRONG-ACCOUNT-ID", MockCustodian.WithdrawalPaymentMethod, MockCustodian.WithdrawalId));
|
||||
|
||||
await AssertHttpError(404, async () => await withdrawalClient.GetCustodianAccountWithdrawalInfo(storeId, "WRONG-ACCOUNT-ID", MockCustodian.WithdrawalPaymentMethod, MockCustodian.WithdrawalId));
|
||||
|
||||
// Test: wrong store ID
|
||||
// TODO shouldn't this be 404? I cannot change this without bigger impact, as it would affect all API endpoints that are store centered
|
||||
await AssertHttpError(403, async () => await withdrawalClient.GetWithdrawalInfo("WRONG-STORE-ID", accountId, MockCustodian.WithdrawalPaymentMethod, MockCustodian.WithdrawalId));
|
||||
|
||||
|
||||
await AssertHttpError(403, async () => await withdrawalClient.GetCustodianAccountWithdrawalInfo("WRONG-STORE-ID", accountId, MockCustodian.WithdrawalPaymentMethod, MockCustodian.WithdrawalId));
|
||||
|
||||
// TODO assert API error codes, not just status codes by using AssertCustodianApiError()
|
||||
|
||||
// TODO also test withdrawals for the various "Status" (Queued, Complete, Failed)
|
||||
// TODO create a mock custodian with only ICustodian
|
||||
// TODO create a mock custodian with only ICustodian + ICanWithdraw
|
||||
@ -4178,12 +4204,11 @@ clientBasic.PreviewUpdateStoreRateConfiguration(user.StoreId, new StoreRateConfi
|
||||
// TODO create a mock custodian with only ICustodian + ICanDeposit
|
||||
}
|
||||
|
||||
private void AssertMockWithdrawal(WithdrawalResponseData withdrawResponse, CustodianAccountData account)
|
||||
private void AssertMockWithdrawal(WithdrawalBaseResponseData withdrawResponse, CustodianAccountData account)
|
||||
{
|
||||
Assert.NotNull(withdrawResponse);
|
||||
Assert.Equal(MockCustodian.WithdrawalAsset, withdrawResponse.Asset);
|
||||
Assert.Equal(MockCustodian.WithdrawalPaymentMethod, withdrawResponse.PaymentMethod);
|
||||
Assert.Equal(MockCustodian.WithdrawalStatus, withdrawResponse.Status);
|
||||
Assert.Equal(account.Id, withdrawResponse.AccountId);
|
||||
Assert.Equal(account.CustodianCode, withdrawResponse.CustodianCode);
|
||||
|
||||
@ -4197,10 +4222,20 @@ clientBasic.PreviewUpdateStoreRateConfiguration(user.StoreId, new StoreRateConfi
|
||||
Assert.Equal(MockCustodian.WithdrawalFee, withdrawResponse.LedgerEntries[1].Qty);
|
||||
Assert.Equal(LedgerEntryData.LedgerEntryType.Fee, withdrawResponse.LedgerEntries[1].Type);
|
||||
|
||||
Assert.Equal(MockCustodian.WithdrawalTargetAddress, withdrawResponse.TargetAddress);
|
||||
Assert.Equal(MockCustodian.WithdrawalTransactionId, withdrawResponse.TransactionId);
|
||||
Assert.Equal(MockCustodian.WithdrawalId, withdrawResponse.WithdrawalId);
|
||||
Assert.NotEqual(default, withdrawResponse.CreatedTime);
|
||||
if (withdrawResponse is WithdrawalResponseData withdrawalResponseData)
|
||||
{
|
||||
Assert.Equal(MockCustodian.WithdrawalStatus, withdrawalResponseData.Status);
|
||||
Assert.Equal(MockCustodian.WithdrawalTargetAddress, withdrawalResponseData.TargetAddress);
|
||||
Assert.Equal(MockCustodian.WithdrawalTransactionId, withdrawalResponseData.TransactionId);
|
||||
Assert.Equal(MockCustodian.WithdrawalId, withdrawalResponseData.WithdrawalId);
|
||||
Assert.NotEqual(default, withdrawalResponseData.CreatedTime);
|
||||
}
|
||||
|
||||
if (withdrawResponse is WithdrawalSimulationResponseData withdrawalSimulationResponseData)
|
||||
{
|
||||
Assert.Equal(MockCustodian.WithdrawalMinAmount, withdrawalSimulationResponseData.MinQty);
|
||||
Assert.Equal(MockCustodian.WithdrawalMaxAmount, withdrawalSimulationResponseData.MaxQty);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Custodians;
|
||||
@ -24,6 +25,9 @@ public class MockCustodian : ICustodian, ICanDeposit, ICanTrade, ICanWithdraw
|
||||
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";
|
||||
@ -52,7 +56,7 @@ public class MockCustodian : ICustodian, ICanDeposit, ICanTrade, ICanWithdraw
|
||||
return Task.FromResult(r);
|
||||
}
|
||||
|
||||
public Task<Form> GetConfigForm(JObject config, string locale, CancellationToken cancellationToken = default)
|
||||
public Task<Form> GetConfigForm(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
@ -135,14 +139,38 @@ public class MockCustodian : ICustodian, ICanDeposit, ICanTrade, ICanWithdraw
|
||||
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> WithdrawAsync(string paymentMethod, decimal amount, JObject config, CancellationToken cancellationToken)
|
||||
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(CreateWithdrawResult());
|
||||
return Task.FromResult(CreateWithdrawSimulationResult());
|
||||
}
|
||||
|
||||
throw new InsufficientFundsException($"{Name} only supports withdrawals of {WithdrawalAmount}");
|
||||
|
@ -2,10 +2,9 @@ using System.Threading.Tasks;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Plugins.PointOfSale;
|
||||
using BTCPayServer.Plugins.PointOfSale.Controllers;
|
||||
using BTCPayServer.Plugins.PointOfSale.Models;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
@ -32,10 +31,11 @@ namespace BTCPayServer.Tests
|
||||
var apps = user.GetController<UIAppsController>();
|
||||
var pos = user.GetController<UIPointOfSaleController>();
|
||||
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp(user.StoreId)).Model);
|
||||
var appType = AppType.PointOfSale.ToString();
|
||||
var appType = PointOfSaleAppType.AppType;
|
||||
vm.AppName = "test";
|
||||
vm.SelectedAppType = appType;
|
||||
Assert.IsType<RedirectToActionResult>(apps.CreateApp(user.StoreId, vm).Result);
|
||||
var redirect = Assert.IsType<RedirectResult>(apps.CreateApp(user.StoreId, vm).Result);
|
||||
Assert.EndsWith("/settings/pos", redirect.Url);
|
||||
var appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
|
||||
var app = appList.Apps[0];
|
||||
var appData = new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName, AppType = appType };
|
||||
|
@ -221,7 +221,7 @@ namespace BTCPayServer.Tests
|
||||
var receiverCoin = await receiverUser.ReceiveUTXO(Money.Satoshis(810), network);
|
||||
|
||||
string errorCode = receiverAddressType == senderAddressType ? null : "unavailable|any UTXO available";
|
||||
var invoice = receiverUser.BitPay.CreateInvoice(new Invoice() { Price = 50000, Currency = "sats", FullNotifications = true });
|
||||
var invoice = receiverUser.BitPay.CreateInvoice(new Invoice() { Price = 50000, Currency = "SATS", FullNotifications = true });
|
||||
if (unsupportedFormats.Contains(receiverAddressType))
|
||||
{
|
||||
Assert.Null(TestAccount.GetPayjoinBitcoinUrl(invoice, cashCow.Network));
|
||||
|
@ -4,11 +4,13 @@ using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Reflection.Metadata;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Lightning;
|
||||
@ -578,8 +580,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.DoesNotContain("invoice-processing", s.Driver.PageSource);
|
||||
});
|
||||
|
||||
Assert.Contains(s.Server.PayTester.GetService<CurrencyNameTable>().DisplayFormatCurrency(100, "USD"),
|
||||
s.Driver.PageSource);
|
||||
Assert.Contains("100.00 USD", s.Driver.PageSource);
|
||||
Assert.Contains(i, s.Driver.PageSource);
|
||||
|
||||
s.GoToInvoices(s.StoreId);
|
||||
@ -832,6 +833,105 @@ namespace BTCPayServer.Tests
|
||||
AssertUrlHasPairingCode(s);
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
public async Task CookieReflectProperPermissions()
|
||||
{
|
||||
using var s = CreateSeleniumTester();
|
||||
await s.StartAsync();
|
||||
var alice = s.Server.NewAccount();
|
||||
alice.Register(false);
|
||||
await alice.CreateStoreAsync();
|
||||
var bob = s.Server.NewAccount();
|
||||
await bob.CreateStoreAsync();
|
||||
await bob.AddGuest(alice.UserId);
|
||||
|
||||
s.GoToLogin();
|
||||
s.LogIn(alice.Email, alice.Password);
|
||||
s.GoToUrl($"/cheat/permissions/stores/{bob.StoreId}");
|
||||
var pageSource = s.Driver.PageSource;
|
||||
AssertPermissions(pageSource, true,
|
||||
new[]
|
||||
{
|
||||
Policies.CanViewInvoices,
|
||||
Policies.CanModifyInvoices,
|
||||
Policies.CanViewPaymentRequests,
|
||||
Policies.CanViewStoreSettings,
|
||||
Policies.CanModifyStoreSettingsUnscoped,
|
||||
Policies.CanDeleteUser
|
||||
});
|
||||
AssertPermissions(pageSource, false,
|
||||
new[]
|
||||
{
|
||||
Policies.CanModifyStoreSettings,
|
||||
Policies.CanCreateNonApprovedPullPayments,
|
||||
Policies.CanCreatePullPayments,
|
||||
Policies.CanManagePullPayments,
|
||||
Policies.CanModifyServerSettings
|
||||
});
|
||||
|
||||
s.GoToUrl($"/cheat/permissions/stores/{alice.StoreId}");
|
||||
pageSource = s.Driver.PageSource;
|
||||
|
||||
AssertPermissions(pageSource, true,
|
||||
new[]
|
||||
{
|
||||
Policies.CanViewInvoices,
|
||||
Policies.CanModifyInvoices,
|
||||
Policies.CanViewPaymentRequests,
|
||||
Policies.CanViewStoreSettings,
|
||||
Policies.CanModifyStoreSettingsUnscoped,
|
||||
Policies.CanDeleteUser,
|
||||
Policies.CanModifyStoreSettings,
|
||||
Policies.CanCreateNonApprovedPullPayments,
|
||||
Policies.CanCreatePullPayments,
|
||||
Policies.CanManagePullPayments
|
||||
});
|
||||
AssertPermissions(pageSource, false,
|
||||
new[]
|
||||
{
|
||||
Policies.CanModifyServerSettings
|
||||
});
|
||||
|
||||
await alice.MakeAdmin();
|
||||
s.Logout();
|
||||
s.GoToLogin();
|
||||
s.LogIn(alice.Email, alice.Password);
|
||||
s.GoToUrl($"/cheat/permissions/stores/{alice.StoreId}");
|
||||
pageSource = s.Driver.PageSource;
|
||||
|
||||
AssertPermissions(pageSource, true,
|
||||
new[]
|
||||
{
|
||||
Policies.CanViewInvoices,
|
||||
Policies.CanModifyInvoices,
|
||||
Policies.CanViewPaymentRequests,
|
||||
Policies.CanViewStoreSettings,
|
||||
Policies.CanModifyStoreSettingsUnscoped,
|
||||
Policies.CanDeleteUser,
|
||||
Policies.CanModifyStoreSettings,
|
||||
Policies.CanCreateNonApprovedPullPayments,
|
||||
Policies.CanCreatePullPayments,
|
||||
Policies.CanManagePullPayments,
|
||||
Policies.CanModifyServerSettings,
|
||||
Policies.CanCreateUser,
|
||||
Policies.CanManageUsers
|
||||
});
|
||||
}
|
||||
|
||||
void AssertPermissions(string source, bool expected, string[] permissions)
|
||||
{
|
||||
if (expected)
|
||||
{
|
||||
foreach (var p in permissions)
|
||||
Assert.Contains(p + "<", source);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var p in permissions)
|
||||
Assert.DoesNotContain(p + "<", source);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
public async Task CanCreateAppPoS()
|
||||
{
|
||||
@ -1274,11 +1374,11 @@ namespace BTCPayServer.Tests
|
||||
// Can add a label?
|
||||
await TestUtils.EventuallyAsync(async () =>
|
||||
{
|
||||
s.Driver.WaitForElement(By.CssSelector("div.label-manager input ")).Click();
|
||||
s.Driver.WaitForElement(By.CssSelector("div.label-manager input")).Click();
|
||||
await Task.Delay(500);
|
||||
s.Driver.WaitForElement(By.CssSelector("div.label-manager input ")).SendKeys("test-label" + Keys.Enter);
|
||||
s.Driver.WaitForElement(By.CssSelector("div.label-manager input")).SendKeys("test-label" + Keys.Enter);
|
||||
await Task.Delay(500);
|
||||
s.Driver.WaitForElement(By.CssSelector("div.label-manager input ")).SendKeys("label2" + Keys.Enter);
|
||||
s.Driver.WaitForElement(By.CssSelector("div.label-manager input")).SendKeys("label2" + Keys.Enter);
|
||||
});
|
||||
|
||||
TestUtils.Eventually(() =>
|
||||
@ -1470,9 +1570,20 @@ namespace BTCPayServer.Tests
|
||||
Assert.Contains("\"Amount\": \"3.00000000\"", s.Driver.PageSource);
|
||||
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
|
||||
|
||||
// BIP-329 export
|
||||
s.Driver.FindElement(By.Id("ExportDropdownToggle")).Click();
|
||||
s.Driver.FindElement(By.Id("ExportBIP329")).Click();
|
||||
Thread.Sleep(1000);
|
||||
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
|
||||
Assert.Contains(s.WalletId.ToString(), s.Driver.Url);
|
||||
Assert.EndsWith("export?format=bip329", s.Driver.Url);
|
||||
Assert.Contains("{\"type\":\"tx\",\"ref\":\"", s.Driver.PageSource);
|
||||
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
|
||||
|
||||
// CSV export
|
||||
s.Driver.FindElement(By.Id("ExportDropdownToggle")).Click();
|
||||
s.Driver.FindElement(By.Id("ExportCSV")).Click();
|
||||
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
@ -1619,7 +1730,10 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.Navigate().Refresh();
|
||||
Assert.Contains("transaction-label", s.Driver.PageSource);
|
||||
});
|
||||
Assert.Equal("payout", s.Driver.FindElement(By.ClassName("transaction-label")).Text);
|
||||
var labels = s.Driver.FindElements(By.CssSelector("#WalletTransactionsList tr:first-child div.transaction-label"));
|
||||
Assert.Equal(2, labels.Count);
|
||||
Assert.Contains(labels, element => element.Text == "payout");
|
||||
Assert.Contains(labels, element => element.Text == "pull-payment");
|
||||
|
||||
s.GoToStore(s.StoreId, StoreNavPages.Payouts);
|
||||
s.Driver.FindElement(By.Id($"{PayoutState.InProgress}-view")).Click();
|
||||
|
@ -220,8 +220,8 @@ namespace BTCPayServer.Tests
|
||||
RegisterDetails = new RegisterViewModel()
|
||||
{
|
||||
Email = Utils.GenerateEmail(),
|
||||
ConfirmPassword = "Kitten0@",
|
||||
Password = "Kitten0@",
|
||||
ConfirmPassword = Password,
|
||||
Password = Password,
|
||||
IsAdmin = isAdmin
|
||||
};
|
||||
await account.Register(RegisterDetails);
|
||||
@ -240,6 +240,7 @@ namespace BTCPayServer.Tests
|
||||
Email = RegisterDetails.Email;
|
||||
IsAdmin = account.RegisteredAdmin;
|
||||
}
|
||||
public string Password { get; set; } = "Kitten0@";
|
||||
|
||||
public RegisterViewModel RegisterDetails { get; set; }
|
||||
|
||||
|
@ -353,6 +353,11 @@ retry:
|
||||
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "tom-select", "tom-select.complete.min.js").Trim();
|
||||
expected = (await (await client.GetAsync($"https://cdn.jsdelivr.net/npm/tom-select@2.2.2/dist/js/tom-select.complete.min.js")).Content.ReadAsStringAsync()).Trim();
|
||||
Assert.Equal(expected, actual);
|
||||
|
||||
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "dom-confetti", "dom-confetti.min.js").Trim();
|
||||
version = Regex.Match(actual, "Original file: /npm/dom-confetti@([0-9]+.[0-9]+.[0-9]+)/lib/main.js").Groups[1].Value;
|
||||
expected = (await (await client.GetAsync($"https://cdn.jsdelivr.net/npm/dom-confetti@{version}/lib/main.min.js")).Content.ReadAsStringAsync()).Trim();
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
|
||||
string GetFileContent(params string[] path)
|
||||
|
@ -35,6 +35,8 @@ using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Bitcoin;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Payments.PayJoin.Sender;
|
||||
using BTCPayServer.Plugins.PayButton;
|
||||
using BTCPayServer.Plugins.PointOfSale;
|
||||
using BTCPayServer.Plugins.PointOfSale.Controllers;
|
||||
using BTCPayServer.Security.Bitpay;
|
||||
using BTCPayServer.Services;
|
||||
@ -1953,14 +1955,13 @@ namespace BTCPayServer.Tests
|
||||
var apps = user.GetController<UIAppsController>();
|
||||
var apps2 = user2.GetController<UIAppsController>();
|
||||
var pos = user.GetController<UIPointOfSaleController>();
|
||||
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp(user.StoreId)).Model);
|
||||
var appType = AppType.PointOfSale.ToString();
|
||||
Assert.NotNull(vm.SelectedAppType);
|
||||
var appType = PointOfSaleAppType.AppType;
|
||||
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp(user.StoreId, appType)).Model);
|
||||
Assert.Equal(appType, vm.SelectedAppType);
|
||||
Assert.Null(vm.AppName);
|
||||
vm.AppName = "test";
|
||||
vm.SelectedAppType = appType;
|
||||
var redirectToAction = Assert.IsType<RedirectToActionResult>(apps.CreateApp(user.StoreId, vm).Result);
|
||||
Assert.Equal(nameof(pos.UpdatePointOfSale), redirectToAction.ActionName);
|
||||
var redirect = Assert.IsType<RedirectResult>(apps.CreateApp(user.StoreId, vm).Result);
|
||||
Assert.EndsWith("/settings/pos", redirect.Url);
|
||||
var appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
|
||||
var appList2 =
|
||||
Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps2.ListApps(user2.StoreId).Result).Model);
|
||||
@ -1976,7 +1977,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal(user.StoreId, appList.Apps[0].StoreId);
|
||||
Assert.IsType<NotFoundResult>(apps2.DeleteApp(appList.Apps[0].Id));
|
||||
Assert.IsType<ViewResult>(apps.DeleteApp(appList.Apps[0].Id));
|
||||
redirectToAction = Assert.IsType<RedirectToActionResult>(apps.DeleteAppPost(appList.Apps[0].Id).Result);
|
||||
var redirectToAction = Assert.IsType<RedirectToActionResult>(apps.DeleteAppPost(appList.Apps[0].Id).Result);
|
||||
Assert.Equal(nameof(stores.Dashboard), redirectToAction.ActionName);
|
||||
appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
|
||||
Assert.Empty(appList.Apps);
|
||||
|
@ -14,6 +14,7 @@ using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Controllers;
|
||||
using ExchangeSharp;
|
||||
using NBitcoin;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using OpenQA.Selenium;
|
||||
using OpenQA.Selenium.Chrome;
|
||||
@ -72,19 +73,24 @@ namespace BTCPayServer.Tests
|
||||
// // DO NOT RUN IT, THIS WILL ERASE THE CURRENT TRANSIFEX TRANSLATIONS
|
||||
|
||||
// var client = GetTransifexClient();
|
||||
// var translations = JsonTranslation.GetTranslations(TranslationFolder.CheckoutV1);
|
||||
// var translations = JsonTranslation.GetTranslations(TranslationFolder.CheckoutV2);
|
||||
// var enTranslations = translations["en"];
|
||||
// translations.Remove("en");
|
||||
|
||||
// foreach (var t in translations)
|
||||
// {
|
||||
// foreach (var w in t.Value.Words.ToArray())
|
||||
// {
|
||||
// if (w.Value == enTranslations.Words[w.Key])
|
||||
// t.Value.Words[w.Key] = null;
|
||||
// if (t.Value.Words[w.Key] == null)
|
||||
// t.Value.Words[w.Key] = enTranslations.Words[w.Key];
|
||||
// }
|
||||
// t.Value.Words.Remove("code");
|
||||
// t.Value.Words.Remove("NOTICE_WARN");
|
||||
// }
|
||||
// await client.UpdateTranslations(translations);
|
||||
// }
|
||||
//#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
|
||||
//#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
|
||||
///// <summary>
|
||||
///// This utility will copy translations made on checkout v1 to checkout v2
|
||||
@ -245,7 +251,6 @@ retry:
|
||||
{
|
||||
// 1. Generate an API Token on https://www.transifex.com/user/settings/api/
|
||||
// 2. Run "dotnet user-secrets set TransifexAPIToken <youapitoken>"
|
||||
|
||||
await PullTransifexTranslationsCore(TranslationFolder.CheckoutV1);
|
||||
await PullTransifexTranslationsCore(TranslationFolder.CheckoutV2);
|
||||
|
||||
@ -282,6 +287,7 @@ retry:
|
||||
{
|
||||
translation.Words["InvoiceExpired_Body_3"] = string.Empty;
|
||||
}
|
||||
translation.Translate(langTranslations);
|
||||
translation.Save();
|
||||
}
|
||||
catch
|
||||
|
@ -53,7 +53,7 @@
|
||||
<PackageReference Include="Fido2" Version="2.0.2" />
|
||||
<PackageReference Include="Fido2.AspNet" Version="2.0.2" />
|
||||
<PackageReference Include="HtmlSanitizer" Version="5.0.372" />
|
||||
<PackageReference Include="LNURL" Version="0.0.28" />
|
||||
<PackageReference Include="LNURL" Version="0.0.29" />
|
||||
<PackageReference Include="MailKit" Version="3.3.0" />
|
||||
<PackageReference Include="BTCPayServer.NETCore.Plugins.Mvc" Version="1.4.4" />
|
||||
<PackageReference Include="QRCoder" Version="1.4.3" />
|
||||
@ -139,6 +139,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<Watch Include="Views\**\*.*"></Watch>
|
||||
<Watch Remove="Views\UIAccount\CheatPermissions.cshtml" />
|
||||
<Content Update="Views\UIApps\_ViewImports.cshtml">
|
||||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
|
@ -19,7 +19,7 @@ namespace BTCPayServer
|
||||
var bg = ColorTranslator.FromHtml(bgColor);
|
||||
int bgDelta = Convert.ToInt32((bg.R * 0.299) + (bg.G * 0.587) + (bg.B * 0.114));
|
||||
Color color = (255 - bgDelta < nThreshold) ? Color.Black : Color.White;
|
||||
return ColorTranslator.ToHtml(color);
|
||||
return ColorTranslator.ToHtml(color).ToLowerInvariant();
|
||||
}
|
||||
// Borrowed from https://github.com/ManageIQ/guides/blob/master/labels.md
|
||||
public static readonly ColorPalette Default = new ColorPalette(new string[] {
|
||||
@ -59,5 +59,44 @@ namespace BTCPayServer
|
||||
return Labels[num % Labels.Length];
|
||||
}
|
||||
}
|
||||
|
||||
/// https://gist.github.com/zihotki/09fc41d52981fb6f93a81ebf20b35cd5
|
||||
/// <summary>
|
||||
/// Creates color with corrected brightness.
|
||||
/// </summary>
|
||||
/// <param name="color">Color to correct.</param>
|
||||
/// <param name="correctionFactor">The brightness correction factor. Must be between -1 and 1.
|
||||
/// Negative values produce darker colors.</param>
|
||||
/// <returns>
|
||||
/// Corrected <see cref="Color"/> structure.
|
||||
/// </returns>
|
||||
public Color AdjustBrightness(Color color, float correctionFactor)
|
||||
{
|
||||
float red = color.R;
|
||||
float green = color.G;
|
||||
float blue = color.B;
|
||||
|
||||
if (correctionFactor < 0)
|
||||
{
|
||||
correctionFactor = 1 + correctionFactor;
|
||||
red *= correctionFactor;
|
||||
green *= correctionFactor;
|
||||
blue *= correctionFactor;
|
||||
}
|
||||
else
|
||||
{
|
||||
red = (255 - red) * correctionFactor + red;
|
||||
green = (255 - green) * correctionFactor + green;
|
||||
blue = (255 - blue) * correctionFactor + blue;
|
||||
}
|
||||
|
||||
return Color.FromArgb(color.A, (int)red, (int)green, (int)blue);
|
||||
}
|
||||
|
||||
public string AdjustBrightness(string html, float correctionFactor)
|
||||
{
|
||||
var color = AdjustBrightness(ColorTranslator.FromHtml(html), correctionFactor);
|
||||
return ColorTranslator.ToHtml(color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Security.AccessControl;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
@ -6,6 +7,8 @@ using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.ViewComponents;
|
||||
using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
||||
|
||||
namespace BTCPayServer.Components.AppSales;
|
||||
|
||||
@ -24,17 +27,28 @@ public class AppSales : ViewComponent
|
||||
_appService = appService;
|
||||
}
|
||||
|
||||
public async Task<IViewComponentResult> InvokeAsync(AppSalesViewModel vm)
|
||||
public async Task<IViewComponentResult> InvokeAsync(string appId, string appType)
|
||||
{
|
||||
if (vm.App == null)
|
||||
throw new ArgumentNullException(nameof(vm.App));
|
||||
var type = _appService.GetAppType(appType);
|
||||
if (type is not IHasSaleStatsAppType salesAppType || type is not AppBaseType appBaseType)
|
||||
return new HtmlContentViewComponentResult(new StringHtmlContent(string.Empty));
|
||||
var vm = new AppSalesViewModel
|
||||
{
|
||||
Id = appId,
|
||||
AppType = appType,
|
||||
DataUrl = Url.Action("AppSales", "UIApps", new { appId }),
|
||||
InitialRendering = HttpContext.GetAppData()?.Id != appId
|
||||
};
|
||||
if (vm.InitialRendering)
|
||||
return View(vm);
|
||||
|
||||
var stats = await _appService.GetSalesStats(vm.App);
|
||||
|
||||
|
||||
var app = HttpContext.GetAppData();
|
||||
var stats = await _appService.GetSalesStats(app);
|
||||
vm.SalesCount = stats.SalesCount;
|
||||
vm.Series = stats.Series;
|
||||
vm.AppType = app.AppType;
|
||||
vm.AppUrl = await appBaseType.ConfigureLink(app);
|
||||
vm.Name = app.Name;
|
||||
|
||||
return View(vm);
|
||||
}
|
||||
|
@ -1,14 +1,17 @@
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services.Apps;
|
||||
|
||||
namespace BTCPayServer.Components.AppSales;
|
||||
|
||||
public class AppSalesViewModel
|
||||
{
|
||||
public AppData App { get; set; }
|
||||
public AppSalesPeriod Period { get; set; } = AppSalesPeriod.Week;
|
||||
public int SalesCount { get; set; }
|
||||
public string Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string AppType { get; set; }
|
||||
public AppSalesPeriod Period { get; set; }
|
||||
public string AppUrl { get; set; }
|
||||
public string DataUrl { get; set; }
|
||||
public long SalesCount { get; set; }
|
||||
public IEnumerable<SalesStatsItem> Series { get; set; }
|
||||
public bool InitialRendering { get; set; }
|
||||
}
|
||||
|
@ -1,17 +1,18 @@
|
||||
@using BTCPayServer.Services.Apps
|
||||
@using BTCPayServer.Components.AppSales
|
||||
@using BTCPayServer.Abstractions.TagHelpers
|
||||
@using BTCPayServer.Plugins.Crowdfund
|
||||
@model BTCPayServer.Components.AppSales.AppSalesViewModel
|
||||
|
||||
@{
|
||||
var controller = $"UI{Model.App.AppType}";
|
||||
var action = $"Update{Model.App.AppType}";
|
||||
var label = Model.App.AppType == nameof(AppType.Crowdfund) ? "Contributions" : "Sales";
|
||||
var label = Model.AppType == CrowdfundAppType.AppType ? "Contributions" : "Sales";
|
||||
}
|
||||
|
||||
<div id="AppSales-@Model.App.Id" class="widget app-sales">
|
||||
<div id="AppSales-@Model.Id" class="widget app-sales">
|
||||
<header class="mb-3">
|
||||
<h3>@Model.App.Name @label</h3>
|
||||
<a asp-controller="@controller" asp-action="@action" asp-route-appId="@Model.App.Id">Manage</a>
|
||||
<h3>@Model.Name @label</h3>
|
||||
@if (!string.IsNullOrEmpty(Model.AppUrl))
|
||||
{
|
||||
<a href="@Model.AppUrl">Manage</a>
|
||||
}
|
||||
</header>
|
||||
@if (Model.InitialRendering)
|
||||
{
|
||||
@ -20,15 +21,16 @@
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
<script src="~/Components/AppSales/Default.cshtml.js" asp-append-version="true"></script>
|
||||
<script>
|
||||
(async () => {
|
||||
const url = @Safe.Json(Url.Action("AppSales", "UIApps", new { appId = Model.App.Id }));
|
||||
const appId = @Safe.Json(Model.App.Id);
|
||||
const url = @Safe.Json(Model.DataUrl);
|
||||
const appId = @Safe.Json(Model.Id);
|
||||
const response = await fetch(url);
|
||||
if (response.ok) {
|
||||
document.getElementById(`AppSales-${appId}`).outerHTML = await response.text();
|
||||
const initScript = document.querySelector(`#AppSales-${appId} script`);
|
||||
if (initScript) eval(initScript.innerHTML);
|
||||
const data = document.querySelector(`#AppSales-${appId} template`);
|
||||
if (data) window.appSales.dataLoaded(JSON.parse(data.innerHTML));
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
@ -40,54 +42,15 @@
|
||||
<span class="sales-count">@Model.SalesCount</span> Total @label
|
||||
</span>
|
||||
<div class="btn-group only-for-js" role="group" aria-label="Filter">
|
||||
<input type="radio" class="btn-check" name="AppSalesPeriod-@Model.App.Id" id="AppSalesPeriodWeek-@Model.App.Id" value="@AppSalesPeriod.Week" @(Model.Period == AppSalesPeriod.Week ? "checked" : "")>
|
||||
<label class="btn btn-link" for="AppSalesPeriodWeek-@Model.App.Id">1W</label>
|
||||
<input type="radio" class="btn-check" name="AppSalesPeriod-@Model.App.Id" id="AppSalesPeriodMonth-@Model.App.Id" value="@AppSalesPeriod.Month" @(Model.Period == AppSalesPeriod.Month ? "checked" : "")>
|
||||
<label class="btn btn-link" for="AppSalesPeriodMonth-@Model.App.Id">1M</label>
|
||||
<input type="radio" class="btn-check" name="AppSalesPeriod-@Model.Id" id="AppSalesPeriodWeek-@Model.Id" value="@AppSalesPeriod.Week" @(Model.Period == AppSalesPeriod.Week ? "checked" : "")>
|
||||
<label class="btn btn-link" for="AppSalesPeriodWeek-@Model.Id">1W</label>
|
||||
<input type="radio" class="btn-check" name="AppSalesPeriod-@Model.Id" id="AppSalesPeriodMonth-@Model.Id" value="@AppSalesPeriod.Month" @(Model.Period == AppSalesPeriod.Month ? "checked" : "")>
|
||||
<label class="btn btn-link" for="AppSalesPeriodMonth-@Model.Id">1M</label>
|
||||
</div>
|
||||
</header>
|
||||
<div class="ct-chart"></div>
|
||||
<script>
|
||||
(function () {
|
||||
const id = @Safe.Json($"AppSales-{Model.App.Id}");
|
||||
const appId = @Safe.Json(Model.App.Id);
|
||||
const period = @Safe.Json(Model.Period.ToString());
|
||||
const baseUrl = @Safe.Json(Url.Action("AppSales", "UIApps", new { appId = Model.App.Id }));
|
||||
const data = { series: @Safe.Json(Model.Series), salesCount: @Safe.Json(Model.SalesCount) };
|
||||
|
||||
const render = (data, period) => {
|
||||
const series = data.series.map(s => s.salesCount);
|
||||
const labels = data.series.map((s, i) => period === @Safe.Json(Model.Period.ToString()) ? s.label : (i % 5 === 0 ? s.label : ''));
|
||||
const min = Math.min(...series);
|
||||
const max = Math.max(...series);
|
||||
const low = min === max ? 0 : Math.max(min - ((max - min) / 5), 0);
|
||||
|
||||
document.querySelectorAll(`#${id} .sales-count`).innerText = data.salesCount;
|
||||
|
||||
new Chartist.Bar(`#${id} .ct-chart`, {
|
||||
labels,
|
||||
series: [series]
|
||||
}, {
|
||||
low,
|
||||
});
|
||||
};
|
||||
|
||||
render(data, period);
|
||||
|
||||
const update = async period => {
|
||||
const url = `${baseUrl}/${period}`;
|
||||
const response = await fetch(url);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
render(data, period);
|
||||
}
|
||||
};
|
||||
|
||||
delegate('change', `#${id} [name="AppSalesPeriod-${appId}"]`, async e => {
|
||||
const type = e.target.value;
|
||||
await update(type);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<template>
|
||||
@Safe.Json(Model)
|
||||
</template>
|
||||
}
|
||||
</div>
|
||||
|
45
BTCPayServer/Components/AppSales/Default.cshtml.js
Normal file
45
BTCPayServer/Components/AppSales/Default.cshtml.js
Normal file
@ -0,0 +1,45 @@
|
||||
if (!window.appSales) {
|
||||
window.appSales =
|
||||
{
|
||||
dataLoaded: function (model) {
|
||||
const id = "AppSales-" + model.id;
|
||||
const appId = model.id;
|
||||
const period = model.period;
|
||||
const baseUrl = model.url;
|
||||
const data = model;
|
||||
|
||||
const render = (data, period) => {
|
||||
const series = data.series.map(s => s.salesCount);
|
||||
const labels = data.series.map((s, i) => period === model.period ? s.label : (i % 5 === 0 ? s.label : ''));
|
||||
const min = Math.min(...series);
|
||||
const max = Math.max(...series);
|
||||
const low = min === max ? 0 : Math.max(min - ((max - min) / 5), 0);
|
||||
|
||||
document.querySelectorAll(`#${id} .sales-count`).innerText = data.salesCount;
|
||||
|
||||
new Chartist.Bar(`#${id} .ct-chart`, {
|
||||
labels,
|
||||
series: [series]
|
||||
}, {
|
||||
low,
|
||||
});
|
||||
};
|
||||
|
||||
render(data, period);
|
||||
|
||||
const update = async period => {
|
||||
const url = `${baseUrl}/${period}`;
|
||||
const response = await fetch(url);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
render(data, period);
|
||||
}
|
||||
};
|
||||
|
||||
delegate('change', `#${id} [name="AppSalesPeriod-${appId}"]`, async e => {
|
||||
const type = e.target.value;
|
||||
await update(type);
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
@ -1,11 +1,14 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Components.AppSales;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.ViewComponents;
|
||||
using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
||||
|
||||
namespace BTCPayServer.Components.AppTopItems;
|
||||
|
||||
@ -18,18 +21,29 @@ public class AppTopItems : ViewComponent
|
||||
_appService = appService;
|
||||
}
|
||||
|
||||
public async Task<IViewComponentResult> InvokeAsync(AppTopItemsViewModel vm)
|
||||
public async Task<IViewComponentResult> InvokeAsync(string appId, string appType)
|
||||
{
|
||||
if (vm.App == null)
|
||||
throw new ArgumentNullException(nameof(vm.App));
|
||||
var type = _appService.GetAppType(appType);
|
||||
if (type is not IHasItemStatsAppType salesAppType || type is not AppBaseType appBaseType)
|
||||
return new HtmlContentViewComponentResult(new StringHtmlContent(string.Empty));
|
||||
|
||||
var vm = new AppTopItemsViewModel
|
||||
{
|
||||
Id = appId,
|
||||
AppType = appType,
|
||||
DataUrl = Url.Action("AppTopItems", "UIApps", new { appId }),
|
||||
InitialRendering = HttpContext.GetAppData()?.Id != appId
|
||||
};
|
||||
if (vm.InitialRendering)
|
||||
return View(vm);
|
||||
|
||||
var entries = Enum.Parse<AppType>(vm.App.AppType) == AppType.Crowdfund
|
||||
? await _appService.GetPerkStats(vm.App)
|
||||
: await _appService.GetItemStats(vm.App);
|
||||
|
||||
var app = HttpContext.GetAppData();
|
||||
var entries = await _appService.GetItemStats(app);
|
||||
vm.SalesCount = entries.Select(e => e.SalesCount).ToList();
|
||||
vm.Entries = entries.ToList();
|
||||
vm.AppType = app.AppType;
|
||||
vm.AppUrl = await appBaseType.ConfigureLink(app);
|
||||
vm.Name = app.Name;
|
||||
|
||||
return View(vm);
|
||||
}
|
||||
|
@ -1,12 +1,16 @@
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services.Apps;
|
||||
|
||||
namespace BTCPayServer.Components.AppTopItems;
|
||||
|
||||
public class AppTopItemsViewModel
|
||||
{
|
||||
public AppData App { get; set; }
|
||||
public string Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string AppType { get; set; }
|
||||
public string AppUrl { get; set; }
|
||||
public string DataUrl { get; set; }
|
||||
public List<ItemStats> Entries { get; set; }
|
||||
public List<int> SalesCount { get; set; }
|
||||
public bool InitialRendering { get; set; }
|
||||
}
|
||||
|
@ -1,16 +1,16 @@
|
||||
@using BTCPayServer.Services.Apps
|
||||
@using BTCPayServer.Plugins.Crowdfund
|
||||
@model BTCPayServer.Components.AppTopItems.AppTopItemsViewModel
|
||||
|
||||
@{
|
||||
var controller = $"UI{Model.App.AppType}";
|
||||
var action = $"Update{Model.App.AppType}";
|
||||
var label = Model.App.AppType == nameof(AppType.Crowdfund) ? "contribution" : "sale";
|
||||
var label = Model.AppType == CrowdfundAppType.AppType ? "contribution" : "sale";
|
||||
}
|
||||
|
||||
<div id="AppTopItems-@Model.App.Id" class="widget app-top-items">
|
||||
<div id="AppTopItems-@Model.Id" class="widget app-top-items">
|
||||
<header class="mb-3">
|
||||
<h3>Top @(Model.App.AppType == nameof(AppType.Crowdfund) ? "Perks" : "Items")</h3>
|
||||
<a asp-controller="@controller" asp-action="@action" asp-route-appId="@Model.App.Id">View All</a>
|
||||
<h3>Top @(Model.AppType == CrowdfundAppType.AppType ? "Perks" : "Items")</h3>
|
||||
@if (!string.IsNullOrEmpty(Model.AppUrl))
|
||||
{
|
||||
<a href="@Model.AppUrl">View All</a>
|
||||
}
|
||||
</header>
|
||||
@if (Model.InitialRendering)
|
||||
{
|
||||
@ -19,37 +19,26 @@
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
<script src="~/Components/AppTopItems/Default.cshtml.js" asp-append-version="true"></script>
|
||||
<script>
|
||||
(async () => {
|
||||
const url = @Safe.Json(Url.Action("AppTopItems", "UIApps", new { appId = Model.App.Id }));
|
||||
const appId = @Safe.Json(Model.App.Id);
|
||||
const response = await fetch(url);
|
||||
if (response.ok) {
|
||||
document.getElementById(`AppTopItems-${appId}`).outerHTML = await response.text();
|
||||
const initScript = document.querySelector(`#AppTopItems-${appId} script`);
|
||||
if (initScript) eval(initScript.innerHTML);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
(async () => {
|
||||
const url = @Safe.Json(Model.DataUrl);
|
||||
const appId = @Safe.Json(Model.Id);
|
||||
const response = await fetch(url);
|
||||
if (response.ok) {
|
||||
document.getElementById(`AppTopItems-${appId}`).outerHTML = await response.text();
|
||||
const data = document.querySelector(`#AppSales-${appId} template`);
|
||||
if (data) window.appSales.dataLoaded(JSON.parse(data.innerHTML));
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
}
|
||||
else if (Model.Entries.Any())
|
||||
{
|
||||
<div class="ct-chart mb-3"></div>
|
||||
<script>
|
||||
(function () {
|
||||
const id = @Safe.Json($"AppTopItems-{Model.App.Id}");
|
||||
const series = @Safe.Json(Model.Entries.Select(i => i.SalesCount));
|
||||
new Chartist.Bar(`#${id} .ct-chart`, { series }, {
|
||||
distributeSeries: true,
|
||||
horizontalBars: true,
|
||||
showLabel: false,
|
||||
stackBars: true,
|
||||
axisY: {
|
||||
offset: 0
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<template>
|
||||
@Safe.Json(Model)
|
||||
</template>
|
||||
<div class="app-items">
|
||||
@for (var i = 0; i < Model.Entries.Count; i++)
|
||||
{
|
||||
|
18
BTCPayServer/Components/AppTopItems/Default.cshtml.js
Normal file
18
BTCPayServer/Components/AppTopItems/Default.cshtml.js
Normal file
@ -0,0 +1,18 @@
|
||||
if (!window.appTopItems) {
|
||||
window.appTopItems =
|
||||
{
|
||||
dataLoaded: function (model) {
|
||||
const id = "AppTopItems-" + model.id;
|
||||
const series = model.salesCount;
|
||||
new Chartist.Bar(`#${id} .ct-chart`, { series }, {
|
||||
distributeSeries: true,
|
||||
horizontalBars: true,
|
||||
showLabel: false,
|
||||
stackBars: true,
|
||||
axisY: {
|
||||
offset: 0
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
@ -1,104 +1,22 @@
|
||||
@using NBitcoin.DataEncoders
|
||||
@using NBitcoin
|
||||
@using BTCPayServer.Abstractions.TagHelpers
|
||||
@model BTCPayServer.Components.LabelManager.LabelViewModel
|
||||
@inject Microsoft.AspNetCore.Antiforgery.IAntiforgery Antiforgery
|
||||
@{
|
||||
var commonCall = Model.ObjectId.Type + Model.ObjectId.Id;
|
||||
var elementId = "a" + Encoders.Base58.EncodeData(RandomUtils.GetBytes(16));
|
||||
var fetchUrl = Url.Action("GetLabels", "UIWallets", new {
|
||||
walletId = Model.WalletObjectId.WalletId,
|
||||
excludeTypes = Safe.Json(Model.ExcludeTypes)
|
||||
});
|
||||
var updateUrl = Model.AutoUpdate? Url.Action("UpdateLabels", "UIWallets", new {
|
||||
walletId = Model.WalletObjectId.WalletId
|
||||
}): string.Empty;
|
||||
}
|
||||
|
||||
<link href="~/vendor/tom-select/tom-select.bootstrap5.min.css" rel="stylesheet">
|
||||
<script src="~/vendor/tom-select/tom-select.complete.min.js"></script>
|
||||
<script>
|
||||
const updateUrl = @Safe.Json(Url.Action("UpdateLabels", "UIWallets", new {
|
||||
Model.ObjectId.WalletId
|
||||
}));
|
||||
const getUrl = @Safe.Json(@Url.Action("GetLabels", "UIWallets", new {
|
||||
walletId = Model.ObjectId.WalletId,
|
||||
excludeTypes = true
|
||||
}));
|
||||
const commonCall = @Safe.Json(commonCall);
|
||||
const elementId = @Safe.Json(elementId);
|
||||
if (!window[commonCall]) {
|
||||
window[commonCall] = fetch(getUrl, {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
}).then(response => {
|
||||
return response.json();
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
const element = document.querySelector(`#${elementId}`);
|
||||
|
||||
if (element) {
|
||||
const labelsFetchTask = await window[commonCall];
|
||||
const config = {
|
||||
create: true,
|
||||
items: @Safe.Json(Model.SelectedLabels),
|
||||
options: labelsFetchTask,
|
||||
valueField: "label",
|
||||
labelField: "label",
|
||||
searchField: "label",
|
||||
allowEmptyOption: false,
|
||||
closeAfterSelect: false,
|
||||
persist: true,
|
||||
render: {
|
||||
option: function(data, escape) {
|
||||
return `<div ${data.color? `style='background-color:${data.color}; color:${data.textColor}'`: ""}>${escape(data.label)}</div>`;
|
||||
},
|
||||
item: function(data, escape) {
|
||||
return `<div ${data.color? `style='background-color:${data.color}; color:${data.textColor}'`: ""}>${escape(data.label)}</div>`;
|
||||
}
|
||||
},
|
||||
onItemAdd: (val) => {
|
||||
window[commonCall] = window[commonCall].then(labels => {
|
||||
return [...labels, { label: val }]
|
||||
});
|
||||
|
||||
document.dispatchEvent(new CustomEvent(`${commonCall}-option-added`, {
|
||||
detail: val
|
||||
}));
|
||||
},
|
||||
onChange: async (values) => {
|
||||
select.lock();
|
||||
try {
|
||||
const response = await fetch(updateUrl, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
address: @Safe.Json(Model.ObjectId.Id),
|
||||
labels: select.items
|
||||
})
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not OK');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('There has been a problem with your fetch operation:', error);
|
||||
} finally {
|
||||
select.unlock();
|
||||
}
|
||||
}
|
||||
};
|
||||
const select = new TomSelect(element, config);
|
||||
|
||||
document.addEventListener(`${commonCall}-option-added`, evt => {
|
||||
if (!(evt.detail in select.options)) {
|
||||
select.addOption({
|
||||
label: evt.detail
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<input id="@elementId" placeholder="Select labels to associate with this object" autocomplete="off" class="form-control label-manager"/>
|
||||
<input id="@elementId" placeholder="Select labels" autocomplete="off" value="@string.Join(",", Model.SelectedLabels)"
|
||||
class="only-for-js form-control label-manager ts-wrapper @(Model.DisplayInline ? "ts-inline" : "")"
|
||||
data-fetch-url="@fetchUrl"
|
||||
data-update-url="@updateUrl"
|
||||
data-wallet-id="@Model.WalletObjectId.WalletId"
|
||||
data-wallet-object-id="@Model.WalletObjectId.Id"
|
||||
data-wallet-object-type="@Model.WalletObjectId.Type"
|
||||
data-select-element="@Model.SelectElement"
|
||||
data-labels='@Safe.Json(Model.RichLabelInfo)' />
|
||||
|
@ -1,3 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
@ -5,14 +7,25 @@ namespace BTCPayServer.Components.LabelManager
|
||||
{
|
||||
public class LabelManager : ViewComponent
|
||||
{
|
||||
public IViewComponentResult Invoke(WalletObjectId walletObjectId, string[] selectedLabels)
|
||||
public IViewComponentResult Invoke(WalletObjectId walletObjectId, string[] selectedLabels, bool excludeTypes = true, bool displayInline = false, Dictionary<string, RichLabelInfo> richLabelInfo = null, bool autoUpdate = true, string selectElement = null)
|
||||
{
|
||||
var vm = new LabelViewModel
|
||||
{
|
||||
ObjectId = walletObjectId,
|
||||
SelectedLabels = selectedLabels
|
||||
ExcludeTypes = excludeTypes,
|
||||
WalletObjectId = walletObjectId,
|
||||
SelectedLabels = selectedLabels?? Array.Empty<string>(),
|
||||
DisplayInline = displayInline,
|
||||
RichLabelInfo = richLabelInfo,
|
||||
AutoUpdate = autoUpdate,
|
||||
SelectElement = selectElement
|
||||
};
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
|
||||
public class RichLabelInfo
|
||||
{
|
||||
public string Link { get; set; }
|
||||
public string Tooltip { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Services;
|
||||
|
||||
namespace BTCPayServer.Components.LabelManager
|
||||
@ -5,6 +6,11 @@ namespace BTCPayServer.Components.LabelManager
|
||||
public class LabelViewModel
|
||||
{
|
||||
public string[] SelectedLabels { get; set; }
|
||||
public WalletObjectId ObjectId { get; set; }
|
||||
public WalletObjectId WalletObjectId { get; set; }
|
||||
public bool ExcludeTypes { get; set; }
|
||||
public bool DisplayInline { get; set; }
|
||||
public Dictionary<string, RichLabelInfo> RichLabelInfo { get; set; }
|
||||
public bool AutoUpdate { get; set; }
|
||||
public string SelectElement { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -74,7 +74,7 @@ namespace BTCPayServer.Components.MainNav
|
||||
Id = a.Id,
|
||||
IsOwner = a.IsOwner,
|
||||
AppName = a.AppName,
|
||||
AppType = Enum.Parse<AppType>(a.AppType)
|
||||
AppType = a.AppType
|
||||
}).ToList();
|
||||
|
||||
if (PoliciesSettings.Experimental)
|
||||
|
@ -19,7 +19,7 @@ namespace BTCPayServer.Components.MainNav
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string AppName { get; set; }
|
||||
public AppType AppType { get; set; }
|
||||
public string AppType { get; set; }
|
||||
public bool IsOwner { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,7 @@
|
||||
"invoice_expiredpaidpartial" => "notifications-invoice-failure",
|
||||
"invoice_failedtoconfirm" => "notifications-invoice-failure",
|
||||
"invoice_confirmed" => "notifications-invoice-settled",
|
||||
"invoice_paidafterexpiration" => "notifications-settled",
|
||||
"invoice_paidafterexpiration" => "notifications-invoice-settled",
|
||||
"external-payout-transaction" => "notifications-payout",
|
||||
"payout_awaitingapproval" => "notifications-payout",
|
||||
"payout_awaitingpayment" => "notifications-payout-approved",
|
||||
|
@ -1,6 +1,8 @@
|
||||
@using BTCPayServer.Abstractions.Extensions
|
||||
@using BTCPayServer.Client.Models
|
||||
@using BTCPayServer.Services
|
||||
@using BTCPayServer.Services.Invoices
|
||||
@inject DisplayFormatter DisplayFormatter
|
||||
@model BTCPayServer.Components.StoreRecentInvoices.StoreRecentInvoicesViewModel
|
||||
|
||||
<div class="widget store-recent-invoices" id="StoreRecentInvoices-@Model.Store.Id">
|
||||
@ -63,7 +65,7 @@
|
||||
</span>
|
||||
}
|
||||
</td>
|
||||
<td class="text-end">@invoice.AmountCurrency</td>
|
||||
<td class="text-end">@DisplayFormatter.Currency(invoice.Amount, invoice.Currency)</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
|
@ -7,7 +7,8 @@ public class StoreRecentInvoiceViewModel
|
||||
{
|
||||
public string InvoiceId { get; set; }
|
||||
public string OrderId { get; set; }
|
||||
public string AmountCurrency { get; set; }
|
||||
public decimal Amount { get; set; }
|
||||
public string Currency { get; set; }
|
||||
public InvoiceState Status { get; set; }
|
||||
public DateTimeOffset Date { get; set; }
|
||||
public bool HasRefund { get; set; }
|
||||
|
@ -61,7 +61,8 @@ public class StoreRecentInvoices : ViewComponent
|
||||
HasRefund = invoice.Refunds.Any(),
|
||||
InvoiceId = invoice.Id,
|
||||
OrderId = invoice.Metadata.OrderId ?? string.Empty,
|
||||
AmountCurrency = _currencyNameTable.DisplayFormatCurrency(invoice.Price, invoice.Currency),
|
||||
Amount = invoice.Price,
|
||||
Currency = invoice.Currency
|
||||
}).ToList();
|
||||
|
||||
return View(vm);
|
||||
|
@ -1,4 +1,6 @@
|
||||
@using BTCPayServer.Abstractions.Extensions
|
||||
@using BTCPayServer.Services
|
||||
@inject DisplayFormatter DisplayFormatter
|
||||
@model BTCPayServer.Components.StoreRecentTransactions.StoreRecentTransactionsViewModel
|
||||
|
||||
<div class="widget store-recent-transactions" id="StoreRecentTransactions-@Model.Store.Id">
|
||||
@ -49,11 +51,11 @@
|
||||
</td>
|
||||
@if (tx.Positive)
|
||||
{
|
||||
<td class="text-end text-success">@tx.Balance</td>
|
||||
<td class="text-end text-success">@DisplayFormatter.Currency(tx.Balance, tx.Currency)</td>
|
||||
}
|
||||
else
|
||||
{
|
||||
<td class="text-end text-danger">@tx.Balance</td>
|
||||
<td class="text-end text-danger">@DisplayFormatter.Currency(tx.Balance, tx.Currency)</td>
|
||||
}
|
||||
</tr>
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ namespace BTCPayServer.Components.StoreRecentTransactions;
|
||||
public class StoreRecentTransactionViewModel
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string Currency { get; set; }
|
||||
public string Balance { get; set; }
|
||||
public bool Positive { get; set; }
|
||||
public bool IsConfirmed { get; set; }
|
||||
|
@ -58,6 +58,7 @@ public class StoreRecentTransactions : ViewComponent
|
||||
Id = tx.TransactionId.ToString(),
|
||||
Positive = tx.BalanceChange.GetValue(network) >= 0,
|
||||
Balance = tx.BalanceChange.ShowMoney(network),
|
||||
Currency = vm.CryptoCode,
|
||||
IsConfirmed = tx.Confirmations != 0,
|
||||
Link = string.Format(CultureInfo.InvariantCulture, network.BlockExplorerLink, tx.TransactionId.ToString()),
|
||||
Timestamp = tx.SeenAt
|
||||
|
17
BTCPayServer/Components/TruncateCenter/Default.cshtml
Normal file
17
BTCPayServer/Components/TruncateCenter/Default.cshtml
Normal file
@ -0,0 +1,17 @@
|
||||
@model BTCPayServer.Components.TruncateCenter.TruncateCenterViewModel
|
||||
<span class="truncate-center @Model.Classes">
|
||||
<span class="truncate-center-truncated" @(Model.Truncated != Model.Text ? $"data-bs-toggle=tooltip title={Model.Text}" : "")>@Model.Truncated</span>
|
||||
<span class="truncate-center-text">@Model.Text</span>
|
||||
@if (Model.Copy)
|
||||
{
|
||||
<button type="button" class="btn btn-link p-0" data-clipboard="@Model.Text">
|
||||
<vc:icon symbol="copy" />
|
||||
</button>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(Model.Link))
|
||||
{
|
||||
<a href="@Model.Link" rel="noreferrer noopener" target="_blank">
|
||||
<vc:icon symbol="info" />
|
||||
</a>
|
||||
}
|
||||
</span>
|
29
BTCPayServer/Components/TruncateCenter/TruncateCenter.cs
Normal file
29
BTCPayServer/Components/TruncateCenter/TruncateCenter.cs
Normal file
@ -0,0 +1,29 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Components.TruncateCenter;
|
||||
|
||||
/// <summary>
|
||||
/// Truncates long strings in the center with ellipsis: Turns e.g. a BOLT11 into "lnbcrt7…q2ns60y"
|
||||
/// </summary>
|
||||
/// <param name="text">The full text, e.g. a Bitcoin address or BOLT11</param>
|
||||
/// <param name="link">Optional link, e.g. a block explorer URL</param>
|
||||
/// <param name="classes">Optional additional CSS classes</param>
|
||||
/// <param name="padding">The number of characters to show on each side</param>
|
||||
/// <param name="copy">Display a copy button</param>
|
||||
/// <returns>HTML with truncated string</returns>
|
||||
public class TruncateCenter : ViewComponent
|
||||
{
|
||||
public IViewComponentResult Invoke(string text, string link = null, string classes = null, int padding = 7, bool copy = true)
|
||||
{
|
||||
var vm = new TruncateCenterViewModel
|
||||
{
|
||||
Classes = classes,
|
||||
Padding = padding,
|
||||
Copy = copy,
|
||||
Text = text,
|
||||
Link = link,
|
||||
Truncated = text.Length > 2 * padding ? $"{text[..padding]}…{text[^padding..]}" : text
|
||||
};
|
||||
return View(vm);
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
namespace BTCPayServer.Components.TruncateCenter
|
||||
{
|
||||
public class TruncateCenterViewModel
|
||||
{
|
||||
public string Text { get; set; }
|
||||
public string Truncated { get; set; }
|
||||
public string Classes { get; set; }
|
||||
public string Link { get; set; }
|
||||
public int Padding { get; set; }
|
||||
public bool Copy { get; set; }
|
||||
}
|
||||
}
|
@ -67,11 +67,21 @@ namespace BTCPayServer.Configuration
|
||||
|
||||
if (conf.GetOrDefault<string>("POSTGRES", null) == null)
|
||||
{
|
||||
|
||||
if (conf.GetOrDefault<string>("SQLITEFILE", null) != null)
|
||||
Logs.Configuration.LogWarning("SQLITE backend support is out of support. Please migrate to Postgres by following the following instructions https://github.com/btcpayserver/btcpayserver/blob/master/docs/db-migration.md");
|
||||
if (conf.GetOrDefault<string>("MYSQL", null) != null)
|
||||
Logs.Configuration.LogWarning("MYSQL backend support is out of support. Please migrate to Postgres by following the following instructions (https://github.com/btcpayserver/btcpayserver/blob/master/docs/db-migration.md)");
|
||||
var allowDeprecated = conf.GetOrDefault<bool>("DEPRECATED", false);
|
||||
if (allowDeprecated)
|
||||
{
|
||||
if (conf.GetOrDefault<string>("SQLITEFILE", null) != null)
|
||||
Logs.Configuration.LogWarning("SQLITE backend support is out of support. Please migrate to Postgres by following the following instructions https://github.com/btcpayserver/btcpayserver/blob/master/docs/db-migration.md");
|
||||
if (conf.GetOrDefault<string>("MYSQL", null) != null)
|
||||
Logs.Configuration.LogWarning("MYSQL backend support is out of support. Please migrate to Postgres by following the following instructions (https://github.com/btcpayserver/btcpayserver/blob/master/docs/db-migration.md)");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (conf.GetOrDefault<string>("SQLITEFILE", null) != null)
|
||||
throw new ConfigException("SQLITE backend support is out of support. Please migrate to Postgres by following the following instructions (https://github.com/btcpayserver/btcpayserver/blob/master/docs/db-migration.md). If you don't want to update, you can try to start this instance by using the command line argument --deprecated");
|
||||
if (conf.GetOrDefault<string>("MYSQL", null) != null)
|
||||
throw new ConfigException("MYSQL backend support is out of support. Please migrate to Postgres by following the following instructions (https://github.com/btcpayserver/btcpayserver/blob/master/docs/db-migration.md). If you don't want to update, you can try to start this instance by using the command line argument --deprecated");
|
||||
}
|
||||
}
|
||||
DockerDeployment = conf.GetOrDefault<bool>("dockerdeployment", true);
|
||||
TorrcFile = conf.GetOrDefault<string>("torrcfile", null);
|
||||
@ -148,9 +158,6 @@ namespace BTCPayServer.Configuration
|
||||
}
|
||||
|
||||
DisableRegistration = conf.GetOrDefault<bool>("disable-registration", true);
|
||||
var pluginRemote = conf.GetOrDefault<string>("plugin-remote", null);
|
||||
if (pluginRemote != null)
|
||||
Logs.Configuration.LogWarning("plugin-remote is an obsolete configuration setting, please remove it from configuration");
|
||||
RecommendedPlugins = conf.GetOrDefault("recommended-plugins", "").ToLowerInvariant().Split('\r', '\n', '\t', ' ').Where(s => !string.IsNullOrEmpty(s)).Distinct().ToArray();
|
||||
CheatMode = conf.GetOrDefault("cheatmode", false);
|
||||
if (CheatMode && this.NetworkType == ChainName.Mainnet)
|
||||
|
@ -30,6 +30,7 @@ namespace BTCPayServer.Configuration
|
||||
app.Option("--mysql", $"DEPRECATED: Connection string to a MySQL database", CommandOptionType.SingleValue);
|
||||
app.Option("--nocsp", $"Disable CSP (default false)", CommandOptionType.BoolValue);
|
||||
app.Option("--sqlitefile", $"DEPRECATED: File name to an SQLite database file inside the data directory", CommandOptionType.SingleValue);
|
||||
app.Option("--deprecated", $"Allow deprecated settings (default:false)", CommandOptionType.BoolValue);
|
||||
app.Option("--externalservices", $"Links added to external services inside Server Settings / Services under the format service1:path2;service2:path2.(default: empty)", CommandOptionType.SingleValue);
|
||||
app.Option("--rootpath", "The root path in the URL to access BTCPay (default: /)", CommandOptionType.SingleValue);
|
||||
app.Option("--sshconnection", "SSH server to manage BTCPay under the form user@server:port (default: root@externalhost or empty)", CommandOptionType.SingleValue);
|
||||
@ -45,7 +46,6 @@ namespace BTCPayServer.Configuration
|
||||
app.Option("--debuglog", "A rolling log file for debug messages.", CommandOptionType.SingleValue);
|
||||
app.Option("--debugloglevel", "The severity you log (default:information)", CommandOptionType.SingleValue);
|
||||
app.Option("--disable-registration", "Disables new user registrations (default:true)", CommandOptionType.SingleValue);
|
||||
app.Option("--plugin-remote", "Obsolete, do not use", CommandOptionType.SingleValue);
|
||||
app.Option("--recommended-plugins", "Plugins which would be marked as recommended to be installed. Separated by newline or space", CommandOptionType.MultipleValue);
|
||||
app.Option("--xforwardedproto", "If specified, set X-Forwarded-Proto to the specified value, this may be useful if your reverse proxy handle https but is not configured to add X-Forwarded-Proto (example: --xforwardedproto https)", CommandOptionType.SingleValue);
|
||||
app.Option("--cheatmode", "Add some helper UI to facilitate dev-time testing (Default false)", CommandOptionType.BoolValue);
|
||||
|
@ -7,6 +7,8 @@ using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Plugins.Crowdfund;
|
||||
using BTCPayServer.Plugins.PointOfSale;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
@ -15,6 +17,7 @@ using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Newtonsoft.Json;
|
||||
using PosViewType = BTCPayServer.Plugins.PointOfSale.PosViewType;
|
||||
|
||||
namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
@ -63,7 +66,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
StoreDataId = storeId,
|
||||
Name = request.AppName,
|
||||
AppType = AppType.Crowdfund.ToString()
|
||||
AppType = CrowdfundAppType.AppType
|
||||
};
|
||||
|
||||
appData.SetSettings(ToCrowdfundSettings(request));
|
||||
@ -94,7 +97,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
StoreDataId = storeId,
|
||||
Name = request.AppName,
|
||||
AppType = AppType.PointOfSale.ToString()
|
||||
AppType = PointOfSaleAppType.AppType
|
||||
};
|
||||
|
||||
appData.SetSettings(ToPointOfSaleSettings(request));
|
||||
@ -108,7 +111,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public async Task<IActionResult> UpdatePointOfSaleApp(string appId, CreatePointOfSaleAppRequest request)
|
||||
{
|
||||
var app = await _appService.GetApp(appId, AppType.PointOfSale);
|
||||
var app = await _appService.GetApp(appId, PointOfSaleAppType.AppType);
|
||||
if (app == null)
|
||||
{
|
||||
return AppNotFound();
|
||||
@ -181,7 +184,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public async Task<IActionResult> GetPosApp(string appId)
|
||||
{
|
||||
var app = await _appService.GetApp(appId, AppType.PointOfSale);
|
||||
var app = await _appService.GetApp(appId, PointOfSaleAppType.AppType);
|
||||
if (app == null)
|
||||
{
|
||||
return AppNotFound();
|
||||
@ -194,7 +197,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public async Task<IActionResult> GetCrowdfundApp(string appId)
|
||||
{
|
||||
var app = await _appService.GetApp(appId, AppType.Crowdfund);
|
||||
var app = await _appService.GetApp(appId, CrowdfundAppType.AppType);
|
||||
if (app == null)
|
||||
{
|
||||
return AppNotFound();
|
||||
@ -242,7 +245,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
EmbeddedCSS = request.EmbeddedCSS?.Trim(),
|
||||
NotificationUrl = request.NotificationUrl?.Trim(),
|
||||
Tagline = request.Tagline?.Trim(),
|
||||
PerksTemplate = request.PerksTemplate != null ? _appService.SerializeTemplate(_appService.Parse(request.PerksTemplate?.Trim(), request.TargetCurrency)) : null,
|
||||
PerksTemplate = request.PerksTemplate is not null ? _appService.SerializeTemplate(_appService.Parse(request.PerksTemplate.Trim(), request.TargetCurrency!)) : null,
|
||||
// If Disqus shortname is not null or empty we assume that Disqus should be enabled
|
||||
DisqusEnabled = !string.IsNullOrEmpty(request.DisqusShortname?.Trim()),
|
||||
DisqusShortname = request.DisqusShortname?.Trim(),
|
||||
@ -264,7 +267,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
return new PointOfSaleSettings()
|
||||
{
|
||||
Title = request.Title,
|
||||
DefaultView = (Services.Apps.PosViewType)request.DefaultView,
|
||||
DefaultView = (PosViewType) request.DefaultView,
|
||||
ShowCustomAmount = request.ShowCustomAmount,
|
||||
ShowDiscount = request.ShowDiscount,
|
||||
EnableTips = request.EnableTips,
|
||||
@ -360,7 +363,8 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
try
|
||||
{
|
||||
_appService.SerializeTemplate(_appService.Parse(request.Template, request.Currency));
|
||||
// Just checking if we can serialize, we don't care about the currency
|
||||
_appService.SerializeTemplate(_appService.Parse(request.Template, "USD"));
|
||||
}
|
||||
catch
|
||||
{
|
||||
@ -449,7 +453,8 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
|
||||
try
|
||||
{
|
||||
_appService.SerializeTemplate(_appService.Parse(request.PerksTemplate, request.TargetCurrency));
|
||||
// Just checking if we can serialize, we don't care about the currency
|
||||
_appService.SerializeTemplate(_appService.Parse(request.PerksTemplate, "USD"));
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
@ -13,6 +13,7 @@ using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.Custodian;
|
||||
using BTCPayServer.Services.Custodian.Client;
|
||||
@ -20,6 +21,7 @@ using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using CustodianAccountData = BTCPayServer.Data.CustodianAccountData;
|
||||
using CustodianAccountDataClient = BTCPayServer.Client.Models.CustodianAccountData;
|
||||
@ -221,6 +223,12 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
|
||||
if (custodian is ICanDeposit depositableCustodian)
|
||||
{
|
||||
var pm = PaymentMethodId.TryParse(paymentMethod);
|
||||
if (pm == null)
|
||||
{
|
||||
return this.CreateAPIError(400, "unsupported-payment-method",
|
||||
$"Unsupported payment method.");
|
||||
}
|
||||
var result = await depositableCustodian.GetDepositAddressAsync(paymentMethod, config, cancellationToken);
|
||||
return Ok(result);
|
||||
}
|
||||
@ -338,6 +346,44 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
$"Fetching past trade info on \"{custodian.Name}\" is not supported.");
|
||||
}
|
||||
|
||||
[HttpPost("~/api/v1/stores/{storeId}/custodian-accounts/{accountId}/withdrawals/simulation")]
|
||||
[Authorize(Policy = Policies.CanWithdrawFromCustodianAccounts,
|
||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public async Task<IActionResult> SimulateWithdrawal(string storeId, string accountId,
|
||||
WithdrawRequestData request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var custodianAccount = await GetCustodianAccount(storeId, accountId);
|
||||
var custodian = GetCustodianByCode(custodianAccount.CustodianCode);
|
||||
|
||||
if (custodian is ICanWithdraw withdrawableCustodian)
|
||||
{
|
||||
var pm = PaymentMethodId.TryParse(request.PaymentMethod);
|
||||
if (pm == null)
|
||||
{
|
||||
return this.CreateAPIError(400, "unsupported-payment-method",
|
||||
$"Unsupported payment method.");
|
||||
}
|
||||
var asset = pm.CryptoCode;
|
||||
decimal qty;
|
||||
try
|
||||
{
|
||||
qty = await ParseQty(request.Qty, asset, custodianAccount, custodian, cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return UnsupportedAsset(asset, ex.Message);
|
||||
}
|
||||
|
||||
var simulateWithdrawResult =
|
||||
await withdrawableCustodian.SimulateWithdrawalAsync(request.PaymentMethod, qty, custodianAccount.GetBlob(), cancellationToken);
|
||||
var result = new WithdrawalSimulationResponseData(simulateWithdrawResult.PaymentMethod, simulateWithdrawResult.Asset,
|
||||
accountId, custodian.Code, simulateWithdrawResult.LedgerEntries, simulateWithdrawResult.MinQty, simulateWithdrawResult.MaxQty);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
return this.CreateAPIError(400, "withdrawals-not-supported",
|
||||
$"Withdrawals are not supported for \"{custodian.Name}\".");
|
||||
}
|
||||
|
||||
[HttpPost("~/api/v1/stores/{storeId}/custodian-accounts/{accountId}/withdrawals")]
|
||||
[Authorize(Policy = Policies.CanWithdrawFromCustodianAccounts,
|
||||
@ -350,8 +396,25 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
|
||||
if (custodian is ICanWithdraw withdrawableCustodian)
|
||||
{
|
||||
var pm = PaymentMethodId.TryParse(request.PaymentMethod);
|
||||
if (pm == null)
|
||||
{
|
||||
return this.CreateAPIError(400, "unsupported-payment-method",
|
||||
$"Unsupported payment method.");
|
||||
}
|
||||
var asset = pm.CryptoCode;
|
||||
decimal qty;
|
||||
try
|
||||
{
|
||||
qty = await ParseQty(request.Qty, asset, custodianAccount, custodian, cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return UnsupportedAsset(asset, ex.Message);
|
||||
}
|
||||
|
||||
var withdrawResult =
|
||||
await withdrawableCustodian.WithdrawAsync(request.PaymentMethod, request.Qty, custodianAccount.GetBlob(), cancellationToken);
|
||||
await withdrawableCustodian.WithdrawToStoreWalletAsync(request.PaymentMethod, qty, custodianAccount.GetBlob(), cancellationToken);
|
||||
var result = new WithdrawalResponseData(withdrawResult.PaymentMethod, withdrawResult.Asset, withdrawResult.LedgerEntries,
|
||||
withdrawResult.WithdrawalId, accountId, custodian.Code, withdrawResult.Status, withdrawResult.CreatedTime, withdrawResult.TargetAddress, withdrawResult.TransactionId);
|
||||
return Ok(result);
|
||||
@ -361,6 +424,22 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
$"Withdrawals are not supported for \"{custodian.Name}\".");
|
||||
}
|
||||
|
||||
private IActionResult UnsupportedAsset(string asset, string err)
|
||||
{
|
||||
return this.CreateAPIError(400, "invalid-qty", $"It is impossible to use % quantity with this asset ({err})");
|
||||
}
|
||||
|
||||
private async Task<decimal> ParseQty(TradeQuantity qty, string asset, CustodianAccountData custodianAccount, ICustodian custodian, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (qty.Type == TradeQuantity.ValueType.Exact)
|
||||
return qty.Value;
|
||||
// Percentage of current holdings => calculate the amount
|
||||
var config = custodianAccount.GetBlob();
|
||||
var balances = await custodian.GetAssetBalancesAsync(config, cancellationToken);
|
||||
if (!balances.TryGetValue(asset, out var assetBalance))
|
||||
return 0.0m;
|
||||
return (assetBalance * qty.Value) / 100m;
|
||||
}
|
||||
|
||||
async Task<CustodianAccountData> GetCustodianAccount(string storeId, string accountId)
|
||||
{
|
||||
|
@ -287,7 +287,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
var lightningClient = await GetLightningClient(cryptoCode, false);
|
||||
var param = new ListInvoicesParams { PendingOnly = pendingOnly, OffsetIndex = offsetIndex };
|
||||
var invoices = await lightningClient.ListInvoices(param, cancellationToken);
|
||||
return Ok(invoices.Select(ToModel));
|
||||
return Ok(invoices.Select(ToModel).ToArray());
|
||||
}
|
||||
|
||||
public virtual async Task<IActionResult> GetPayments(string cryptoCode, [FromQuery] bool? includePending, [FromQuery] long? offsetIndex, CancellationToken cancellationToken = default)
|
||||
@ -295,7 +295,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
var lightningClient = await GetLightningClient(cryptoCode, false);
|
||||
var param = new ListPaymentsParams { IncludePending = includePending, OffsetIndex = offsetIndex };
|
||||
var payments = await lightningClient.ListPayments(param, cancellationToken);
|
||||
return Ok(payments.Select(ToModel));
|
||||
return Ok(payments.Select(ToModel).ToArray());
|
||||
}
|
||||
|
||||
public virtual async Task<IActionResult> CreateInvoice(string cryptoCode, CreateLightningInvoiceRequest request, CancellationToken cancellationToken = default)
|
||||
|
@ -9,6 +9,7 @@ using BTCPayServer.Data;
|
||||
using BTCPayServer.PayoutProcessors;
|
||||
using BTCPayServer.Security;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using StoreData = BTCPayServer.Data.StoreData;
|
||||
using PayoutProcessorData = BTCPayServer.Client.Models.PayoutProcessorData;
|
||||
@ -17,6 +18,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
[ApiController]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
public class GreenfieldPayoutProcessorsController : ControllerBase
|
||||
{
|
||||
private readonly IEnumerable<IPayoutProcessorFactory> _factories;
|
||||
|
10
BTCPayServer/Controllers/GreenField/GreenfieldStoreAutomatedLightningPayoutProcessorsController.cs
10
BTCPayServer/Controllers/GreenField/GreenfieldStoreAutomatedLightningPayoutProcessorsController.cs
@ -10,6 +10,7 @@ using BTCPayServer.PayoutProcessors;
|
||||
using BTCPayServer.PayoutProcessors.Lightning;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using PayoutProcessorData = BTCPayServer.Data.PayoutProcessorData;
|
||||
|
||||
@ -17,6 +18,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
[ApiController]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
public class GreenfieldStoreAutomatedLightningPayoutProcessorsController : ControllerBase
|
||||
{
|
||||
private readonly PayoutProcessorService _payoutProcessorService;
|
||||
@ -30,9 +32,8 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpGet("~/api/v1/stores/{storeId}/payout-processors/" + nameof(LightningAutomatedPayoutSenderFactory))]
|
||||
[HttpGet("~/api/v1/stores/{storeId}/payout-processors/" + nameof(LightningAutomatedPayoutSenderFactory) +
|
||||
"/{paymentMethod}")]
|
||||
[HttpGet("~/api/v1/stores/{storeId}/payout-processors/LightningAutomatedPayoutSenderFactory")]
|
||||
[HttpGet("~/api/v1/stores/{storeId}/payout-processors/LightningAutomatedPayoutSenderFactory/{paymentMethod}")]
|
||||
public async Task<IActionResult> GetStoreLightningAutomatedPayoutProcessors(
|
||||
string storeId, string? paymentMethod)
|
||||
{
|
||||
@ -64,8 +65,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpPut("~/api/v1/stores/{storeId}/payout-processors/" + nameof(LightningAutomatedPayoutSenderFactory) +
|
||||
"/{paymentMethod}")]
|
||||
[HttpPut("~/api/v1/stores/{storeId}/payout-processors/LightningAutomatedPayoutSenderFactory/{paymentMethod}")]
|
||||
public async Task<IActionResult> UpdateStoreLightningAutomatedPayoutProcessor(
|
||||
string storeId, string paymentMethod, LightningAutomatedPayoutSettings request)
|
||||
{
|
||||
|
@ -10,6 +10,7 @@ using BTCPayServer.PayoutProcessors;
|
||||
using BTCPayServer.PayoutProcessors.OnChain;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using PayoutProcessorData = BTCPayServer.Data.PayoutProcessorData;
|
||||
|
||||
@ -17,6 +18,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
[ApiController]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
public class GreenfieldStoreAutomatedOnChainPayoutProcessorsController : ControllerBase
|
||||
{
|
||||
private readonly PayoutProcessorService _payoutProcessorService;
|
||||
@ -30,9 +32,8 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpGet("~/api/v1/stores/{storeId}/payout-processors/" + nameof(OnChainAutomatedPayoutSenderFactory))]
|
||||
[HttpGet("~/api/v1/stores/{storeId}/payout-processors/" + nameof(OnChainAutomatedPayoutSenderFactory) +
|
||||
"/{paymentMethod}")]
|
||||
[HttpGet("~/api/v1/stores/{storeId}/payout-processors/OnChainAutomatedPayoutSenderFactory")]
|
||||
[HttpGet("~/api/v1/stores/{storeId}/payout-processors/OnChainAutomatedPayoutSenderFactory/{paymentMethod}")]
|
||||
public async Task<IActionResult> GetStoreOnChainAutomatedPayoutProcessors(
|
||||
string storeId, string? paymentMethod)
|
||||
{
|
||||
@ -70,8 +71,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpPut("~/api/v1/stores/{storeId}/payout-processors/" + nameof(OnChainAutomatedPayoutSenderFactory) +
|
||||
"/{paymentMethod}")]
|
||||
[HttpPut("~/api/v1/stores/{storeId}/payout-processors/OnChainAutomatedPayoutSenderFactory/{paymentMethod}")]
|
||||
public async Task<IActionResult> UpdateStoreOnchainAutomatedPayoutProcessor(
|
||||
string storeId, string paymentMethod, OnChainAutomatedPayoutSettings request)
|
||||
{
|
||||
|
@ -17,6 +17,7 @@ using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StoreData = BTCPayServer.Data.StoreData;
|
||||
@ -25,24 +26,19 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
[ApiController]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
public class GreenfieldStoreLNURLPayPaymentMethodsController : ControllerBase
|
||||
{
|
||||
private StoreData Store => HttpContext.GetStoreData();
|
||||
private readonly StoreRepository _storeRepository;
|
||||
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly ISettingsRepository _settingsRepository;
|
||||
|
||||
public GreenfieldStoreLNURLPayPaymentMethodsController(
|
||||
StoreRepository storeRepository,
|
||||
BTCPayNetworkProvider btcPayNetworkProvider,
|
||||
IAuthorizationService authorizationService,
|
||||
ISettingsRepository settingsRepository)
|
||||
BTCPayNetworkProvider btcPayNetworkProvider)
|
||||
{
|
||||
_storeRepository = storeRepository;
|
||||
_btcPayNetworkProvider = btcPayNetworkProvider;
|
||||
_authorizationService = authorizationService;
|
||||
_settingsRepository = settingsRepository;
|
||||
}
|
||||
|
||||
public static IEnumerable<LNURLPayPaymentMethodData> GetLNURLPayPaymentMethods(StoreData store,
|
||||
|
@ -6,6 +6,7 @@ using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using AuthenticationSchemes = BTCPayServer.Abstractions.Constants.AuthenticationSchemes;
|
||||
using LightningAddressData = BTCPayServer.Client.Models.LightningAddressData;
|
||||
@ -14,6 +15,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
[ApiController]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
public class GreenfieldStoreLightningAddressesController : ControllerBase
|
||||
{
|
||||
private readonly LightningAddressService _lightningAddressService;
|
||||
|
@ -18,6 +18,7 @@ using BTCPayServer.Security;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StoreData = BTCPayServer.Data.StoreData;
|
||||
@ -26,6 +27,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
[ApiController]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
public class GreenfieldStoreLightningNetworkPaymentMethodsController : ControllerBase
|
||||
{
|
||||
private StoreData Store => HttpContext.GetStoreData();
|
||||
|
@ -8,6 +8,7 @@ using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Payments;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBXplorer.Models;
|
||||
|
||||
@ -17,6 +18,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpPost("~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/generate")]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
public async Task<IActionResult> GenerateOnChainWallet(string storeId, string cryptoCode,
|
||||
GenerateWalletRequest request)
|
||||
{
|
||||
|
@ -14,6 +14,7 @@ using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
@ -24,6 +25,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
[ApiController]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
public partial class GreenfieldStoreOnChainPaymentMethodsController : ControllerBase
|
||||
{
|
||||
private StoreData Store => HttpContext.GetStoreData();
|
||||
|
@ -8,6 +8,7 @@ using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Security;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using StoreData = BTCPayServer.Data.StoreData;
|
||||
|
||||
@ -15,6 +16,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
[ApiController]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
public class GreenfieldStorePaymentMethodsController : ControllerBase
|
||||
{
|
||||
private StoreData Store => HttpContext.GetStoreData();
|
||||
|
@ -7,12 +7,14 @@ using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.PayoutProcessors;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
[ApiController]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
public class GreenfieldStorePayoutProcessorsController : ControllerBase
|
||||
{
|
||||
private readonly PayoutProcessorService _payoutProcessorService;
|
||||
|
@ -13,6 +13,7 @@ using BTCPayServer.Rating;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using RateSource = BTCPayServer.Client.Models.RateSource;
|
||||
|
||||
@ -21,6 +22,7 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
[ApiController]
|
||||
[Route("api/v1/stores/{storeId}/rates/configuration")]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
public class GreenfieldStoreRateConfigurationController : ControllerBase
|
||||
{
|
||||
private readonly RateFetcher _rateProviderFactory;
|
||||
|
@ -11,6 +11,7 @@ using BTCPayServer.Data;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Controllers.GreenField
|
||||
@ -18,6 +19,7 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
[ApiController]
|
||||
[Route("api/v1/stores/{storeId}/rates")]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
public class GreenfieldStoreRatesController : ControllerBase
|
||||
{
|
||||
private readonly RateFetcher _rateProviderFactory;
|
||||
|
@ -6,6 +6,7 @@ using BTCPayServer.Data;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
@ -17,6 +18,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
[Route("api/test/apikey")]
|
||||
[ApiController]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
public class GreenfieldTestApiKeyController : ControllerBase
|
||||
{
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
|
@ -223,6 +223,20 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
return GetFromActionResult<MarketTradeResponseData>(
|
||||
await GetController<GreenfieldCustodianAccountController>().MarketTradeCustodianAccountAsset(storeId, accountId, request, cancellationToken));
|
||||
}
|
||||
|
||||
public override async Task<WithdrawalSimulationResponseData> SimulateCustodianAccountWithdrawal(string storeId, string accountId,
|
||||
WithdrawRequestData request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return GetFromActionResult<WithdrawalSimulationResponseData>(
|
||||
await GetController<GreenfieldCustodianAccountController>().SimulateWithdrawal(storeId, accountId, request, cancellationToken));
|
||||
}
|
||||
|
||||
public override async Task<WithdrawalResponseData> CreateCustodianAccountWithdrawal(string storeId, string accountId,
|
||||
WithdrawRequestData request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return GetFromActionResult<WithdrawalResponseData>(
|
||||
await GetController<GreenfieldCustodianAccountController>().CreateWithdrawal(storeId, accountId, request, cancellationToken));
|
||||
}
|
||||
|
||||
public override async Task<OnChainWalletObjectData[]> GetOnChainWalletObjects(string storeId, string cryptoCode,
|
||||
GetWalletObjectsRequest query = null,
|
||||
|
@ -7,10 +7,12 @@ using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Fido2;
|
||||
using BTCPayServer.Fido2.Models;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Models.AccountViewModels;
|
||||
using BTCPayServer.Services;
|
||||
@ -83,6 +85,24 @@ namespace BTCPayServer.Controllers
|
||||
get; set;
|
||||
}
|
||||
|
||||
[HttpGet("/cheat/permissions")]
|
||||
[HttpGet("/cheat/permissions/stores/{storeId}")]
|
||||
[CheatModeRoute]
|
||||
public async Task<IActionResult> CheatPermissions([FromServices]IAuthorizationService authorizationService, string storeId = null)
|
||||
{
|
||||
var vm = new CheatPermissionsViewModel();
|
||||
vm.StoreId = storeId;
|
||||
var results = new System.Collections.Generic.List<(string, Task<AuthorizationResult>)>();
|
||||
foreach (var p in Policies.AllPolicies.Concat(new[] { Policies.CanModifyStoreSettingsUnscoped }))
|
||||
{
|
||||
results.Add((p, authorizationService.AuthorizeAsync(User, storeId, p)));
|
||||
}
|
||||
await Task.WhenAll(results.Select(r => r.Item2));
|
||||
results = results.OrderBy(r => r.Item1).ToList();
|
||||
vm.Permissions = results.Select(r => (r.Item1, r.Item2.Result)).ToArray();
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[HttpGet("/login")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> Login(string returnUrl = null, string email = null)
|
||||
|
@ -21,8 +21,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
app.StoreData = GetCurrentStore();
|
||||
|
||||
var vm = new AppTopItemsViewModel { App = app };
|
||||
return ViewComponent("AppTopItems", new { vm });
|
||||
return ViewComponent("AppTopItems", new { appId = app.Id, appType = app.AppType });
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
@ -34,9 +33,7 @@ namespace BTCPayServer.Controllers
|
||||
return NotFound();
|
||||
|
||||
app.StoreData = GetCurrentStore();
|
||||
|
||||
var vm = new AppSalesViewModel { App = app };
|
||||
return ViewComponent("AppSales", new { vm });
|
||||
return ViewComponent("AppSales", new { appId = app.Id, appType = app.AppType });
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
|
@ -1,4 +1,3 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
@ -6,8 +5,6 @@ using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Plugins.Crowdfund.Controllers;
|
||||
using BTCPayServer.Plugins.PointOfSale.Controllers;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@ -57,13 +54,14 @@ namespace BTCPayServer.Controllers
|
||||
var app = await _appService.GetApp(appId, null);
|
||||
if (app is null)
|
||||
return NotFound();
|
||||
|
||||
return app.AppType switch
|
||||
|
||||
var res = await _appService.ViewLink(app);
|
||||
if (res is null)
|
||||
{
|
||||
nameof(AppType.Crowdfund) => RedirectToAction(nameof(UICrowdfundController.ViewCrowdfund), "UICrowdfund", new { appId }),
|
||||
nameof(AppType.PointOfSale) => RedirectToAction(nameof(UIPointOfSaleController.ViewPointOfSale), "UIPointOfSale", new { appId }),
|
||||
_ => NotFound()
|
||||
};
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return Redirect(res);
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
@ -114,12 +112,10 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[HttpGet("/stores/{storeId}/apps/create")]
|
||||
public IActionResult CreateApp(string storeId)
|
||||
public IActionResult CreateApp(string storeId, string appType = null)
|
||||
{
|
||||
return View(new CreateAppViewModel
|
||||
{
|
||||
StoreId = GetCurrentStore().Id
|
||||
});
|
||||
var vm = new CreateAppViewModel (_appService){StoreId = GetCurrentStore().Id, SelectedAppType = appType};
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
@ -128,8 +124,8 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
var store = GetCurrentStore();
|
||||
vm.StoreId = store.Id;
|
||||
|
||||
if (!Enum.TryParse(vm.SelectedAppType, out AppType appType))
|
||||
var type = _appService.GetAppType(vm.SelectedAppType);
|
||||
if (type is null)
|
||||
ModelState.AddModelError(nameof(vm.SelectedAppType), "Invalid App Type");
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
@ -141,34 +137,19 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
StoreDataId = store.Id,
|
||||
Name = vm.AppName,
|
||||
AppType = appType.ToString()
|
||||
AppType = vm.SelectedAppType
|
||||
};
|
||||
|
||||
var defaultCurrency = await GetStoreDefaultCurrentIfEmpty(appData.StoreDataId, null);
|
||||
switch (appType)
|
||||
{
|
||||
case AppType.Crowdfund:
|
||||
var emptyCrowdfund = new CrowdfundSettings { TargetCurrency = defaultCurrency };
|
||||
appData.SetSettings(emptyCrowdfund);
|
||||
break;
|
||||
case AppType.PointOfSale:
|
||||
var empty = new PointOfSaleSettings { Currency = defaultCurrency };
|
||||
appData.SetSettings(empty);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
await _appService.SetDefaultSettings(appData, defaultCurrency);
|
||||
await _appService.UpdateOrCreateApp(appData);
|
||||
|
||||
TempData[WellKnownTempData.SuccessMessage] = "App successfully created";
|
||||
CreatedAppId = appData.Id;
|
||||
|
||||
return appType switch
|
||||
{
|
||||
AppType.PointOfSale => RedirectToAction(nameof(UIPointOfSaleController.UpdatePointOfSale), "UIPointOfSale", new { appId = appData.Id }),
|
||||
AppType.Crowdfund => RedirectToAction(nameof(UICrowdfundController.UpdateCrowdfund), "UICrowdfund", new { appId = appData.Id }),
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
|
||||
var url = await type.ConfigureLink(appData);
|
||||
return Redirect(url);
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
|
@ -13,6 +13,7 @@ using BTCPayServer.Data;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Models.CustodianAccountViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Custodian.Client;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@ -20,6 +21,7 @@ using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NLog.Config;
|
||||
using CustodianAccountData = BTCPayServer.Data.CustodianAccountData;
|
||||
using StoreData = BTCPayServer.Data.StoreData;
|
||||
|
||||
@ -32,13 +34,13 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
private readonly IEnumerable<ICustodian> _custodianRegistry;
|
||||
private readonly CustodianAccountRepository _custodianAccountRepository;
|
||||
private readonly CurrencyNameTable _currencyNameTable;
|
||||
private readonly DisplayFormatter _displayFormatter;
|
||||
private readonly BTCPayServerClient _btcPayServerClient;
|
||||
private readonly BTCPayNetworkProvider _networkProvider;
|
||||
private readonly LinkGenerator _linkGenerator;
|
||||
|
||||
public UICustodianAccountsController(
|
||||
CurrencyNameTable currencyNameTable,
|
||||
DisplayFormatter displayFormatter,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
CustodianAccountRepository custodianAccountRepository,
|
||||
IEnumerable<ICustodian> custodianRegistry,
|
||||
@ -47,7 +49,7 @@ namespace BTCPayServer.Controllers
|
||||
LinkGenerator linkGenerator
|
||||
)
|
||||
{
|
||||
_currencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable));
|
||||
_displayFormatter = displayFormatter;
|
||||
_custodianAccountRepository = custodianAccountRepository;
|
||||
_custodianRegistry = custodianRegistry;
|
||||
_btcPayServerClient = btcPayServerClient;
|
||||
@ -144,7 +146,7 @@ namespace BTCPayServer.Controllers
|
||||
if (asset.Equals(defaultCurrency))
|
||||
{
|
||||
assetBalance.FormattedFiatValue =
|
||||
_currencyNameTable.DisplayFormatCurrency(pair.Value.Qty, defaultCurrency);
|
||||
_displayFormatter.Currency(pair.Value.Qty, defaultCurrency);
|
||||
assetBalance.FiatValue = pair.Value.Qty;
|
||||
}
|
||||
else
|
||||
@ -156,11 +158,11 @@ namespace BTCPayServer.Controllers
|
||||
assetBalance.Bid = quote.Bid;
|
||||
assetBalance.Ask = quote.Ask;
|
||||
assetBalance.FormattedBid =
|
||||
_currencyNameTable.DisplayFormatCurrency(quote.Bid, quote.FromAsset);
|
||||
_displayFormatter.Currency(quote.Bid, quote.FromAsset);
|
||||
assetBalance.FormattedAsk =
|
||||
_currencyNameTable.DisplayFormatCurrency(quote.Ask, quote.FromAsset);
|
||||
_displayFormatter.Currency(quote.Ask, quote.FromAsset);
|
||||
assetBalance.FormattedFiatValue =
|
||||
_currencyNameTable.DisplayFormatCurrency(pair.Value.Qty * quote.Bid,
|
||||
_displayFormatter.Currency(pair.Value.Qty * quote.Bid,
|
||||
defaultCurrency);
|
||||
assetBalance.FiatValue = pair.Value.Qty * quote.Bid;
|
||||
}
|
||||
@ -174,14 +176,14 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
if (custodian is ICanWithdraw withdrawableCustodian)
|
||||
{
|
||||
var withdrawableePaymentMethods = withdrawableCustodian.GetWithdrawablePaymentMethods();
|
||||
foreach (var withdrawableePaymentMethod in withdrawableePaymentMethods)
|
||||
var withdrawablePaymentMethods = withdrawableCustodian.GetWithdrawablePaymentMethods();
|
||||
foreach (var withdrawablePaymentMethod in withdrawablePaymentMethods)
|
||||
{
|
||||
var withdrawableAsset = withdrawableePaymentMethod.Split("-")[0];
|
||||
var withdrawableAsset = withdrawablePaymentMethod.Split("-")[0];
|
||||
if (assetBalances.ContainsKey(withdrawableAsset))
|
||||
{
|
||||
var assetBalance = assetBalances[withdrawableAsset];
|
||||
assetBalance.CanWithdraw = true;
|
||||
assetBalance.WithdrawablePaymentMethods.Add(withdrawablePaymentMethod);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -215,7 +217,8 @@ namespace BTCPayServer.Controllers
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var configForm = await custodian.GetConfigForm(custodianAccount.GetBlob(), "en-US");
|
||||
var configForm = await custodian.GetConfigForm();
|
||||
configForm.SetValues(custodianAccount.GetBlob());
|
||||
|
||||
var vm = new EditCustodianAccountViewModel();
|
||||
vm.CustodianAccount = custodianAccount;
|
||||
@ -227,9 +230,6 @@ namespace BTCPayServer.Controllers
|
||||
public async Task<IActionResult> EditCustodianAccount(string storeId, string accountId,
|
||||
EditCustodianAccountViewModel vm)
|
||||
{
|
||||
// The locale is not important yet, but keeping it here so we can find it easily when localization becomes a thing.
|
||||
var locale = "en-US";
|
||||
|
||||
var custodianAccount = await _custodianAccountRepository.FindById(storeId, accountId);
|
||||
if (custodianAccount == null)
|
||||
return NotFound();
|
||||
@ -241,37 +241,22 @@ namespace BTCPayServer.Controllers
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var configForm = await custodian.GetConfigForm(custodianAccount.GetBlob(), locale);
|
||||
var configForm = await custodian.GetConfigForm();
|
||||
configForm.ApplyValuesFromForm(Request.Form);
|
||||
|
||||
|
||||
var newData = new JObject();
|
||||
foreach (var pair in Request.Form)
|
||||
if (configForm.IsValid())
|
||||
{
|
||||
if ("CustodianAccount.Name".Equals(pair.Key))
|
||||
{
|
||||
custodianAccount.Name = pair.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO support posted array notation, like a field called "WithdrawToAddressNamePerPaymentMethod[BTC-OnChain]". The data should be nested in the JSON.
|
||||
newData.Add(pair.Key, pair.Value.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
var newConfigData = RemoveUnusedFieldsFromConfig(custodianAccount.GetBlob(), newData, configForm);
|
||||
var newConfigForm = await custodian.GetConfigForm(newConfigData, locale);
|
||||
|
||||
if (newConfigForm.IsValid())
|
||||
{
|
||||
custodianAccount.SetBlob(newConfigData);
|
||||
var newData = configForm.GetValues();
|
||||
custodianAccount.SetBlob(newData);
|
||||
custodianAccount = await _custodianAccountRepository.CreateOrUpdate(custodianAccount);
|
||||
|
||||
return RedirectToAction(nameof(ViewCustodianAccount),
|
||||
new { storeId = custodianAccount.StoreId, accountId = custodianAccount.Id });
|
||||
}
|
||||
|
||||
// Form not valid: The user must fix the errors before we can save
|
||||
vm.CustodianAccount = custodianAccount;
|
||||
vm.ConfigForm = newConfigForm;
|
||||
vm.ConfigForm = configForm;
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
@ -312,16 +297,11 @@ namespace BTCPayServer.Controllers
|
||||
};
|
||||
|
||||
|
||||
var configData = new JObject();
|
||||
foreach (var pair in Request.Form)
|
||||
{
|
||||
configData.Add(pair.Key, pair.Value.ToString());
|
||||
}
|
||||
|
||||
var configForm = await custodian.GetConfigForm(configData, "en-US");
|
||||
var configForm = await custodian.GetConfigForm();
|
||||
configForm.ApplyValuesFromForm(Request.Form);
|
||||
if (configForm.IsValid())
|
||||
{
|
||||
// configForm.removeUnusedKeys();
|
||||
var configData = configForm.GetValues();
|
||||
custodianAccountData.SetBlob(configData);
|
||||
custodianAccountData = await _custodianAccountRepository.CreateOrUpdate(custodianAccountData);
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Custodian account successfully created";
|
||||
@ -357,37 +337,11 @@ namespace BTCPayServer.Controllers
|
||||
new { storeId = custodianAccount.StoreId, accountId = custodianAccount.Id });
|
||||
}
|
||||
|
||||
// The JObject may contain too much data because we used ALL post values and this may be more than we needed.
|
||||
// Because we don't know the form fields beforehand, we will filter out the superfluous data afterwards.
|
||||
// We will keep all the old keys + merge the new keys as per the current form.
|
||||
// Since the form can differ by circumstances, we will never remove any keys that were previously stored. We just limit what we add.
|
||||
private JObject RemoveUnusedFieldsFromConfig(JObject storedData, JObject newData, Form form)
|
||||
[HttpPost("/stores/{storeId}/custodian-accounts/{accountId}/trade/simulate")]
|
||||
public async Task<IActionResult> SimulateTradeJson(string storeId, string accountId,
|
||||
[FromBody] TradeRequestData request)
|
||||
{
|
||||
JObject filteredData = new JObject();
|
||||
var storedKeys = new List<string>();
|
||||
foreach (var item in storedData)
|
||||
{
|
||||
storedKeys.Add(item.Key);
|
||||
}
|
||||
|
||||
var formKeys = form.GetAllFields().Select(f => f.FullName).ToHashSet();
|
||||
|
||||
foreach (var item in newData)
|
||||
{
|
||||
if (storedKeys.Contains(item.Key) || formKeys.Contains(item.Key))
|
||||
{
|
||||
filteredData[item.Key] = item.Value;
|
||||
}
|
||||
}
|
||||
|
||||
return filteredData;
|
||||
}
|
||||
|
||||
[HttpGet("/stores/{storeId}/custodian-accounts/{accountId}/trade/prepare")]
|
||||
public async Task<IActionResult> GetTradePrepareJson(string storeId, string accountId,
|
||||
[FromQuery] string assetToTrade, [FromQuery] string assetToTradeInto)
|
||||
{
|
||||
if (string.IsNullOrEmpty(assetToTrade) || string.IsNullOrEmpty(assetToTradeInto))
|
||||
if (string.IsNullOrEmpty(request.FromAsset) || string.IsNullOrEmpty(request.ToAsset))
|
||||
{
|
||||
return BadRequest();
|
||||
}
|
||||
@ -421,12 +375,12 @@ namespace BTCPayServer.Controllers
|
||||
foreach (var pair in assetBalancesData)
|
||||
{
|
||||
var oneAsset = pair.Key;
|
||||
if (assetToTrade.Equals(oneAsset))
|
||||
if (request.FromAsset.Equals(oneAsset))
|
||||
{
|
||||
vm.MaxQtyToTrade = pair.Value;
|
||||
vm.MaxQty = pair.Value;
|
||||
//vm.FormattedMaxQtyToTrade = pair.Value;
|
||||
|
||||
if (assetToTrade.Equals(assetToTradeInto))
|
||||
if (request.FromAsset.Equals(request.ToAsset))
|
||||
{
|
||||
// We cannot trade the asset for itself
|
||||
return BadRequest();
|
||||
@ -434,7 +388,8 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
try
|
||||
{
|
||||
var quote = await tradingCustodian.GetQuoteForAssetAsync(assetToTrade, assetToTradeInto,
|
||||
var quote = await tradingCustodian.GetQuoteForAssetAsync(request.FromAsset,
|
||||
request.ToAsset,
|
||||
config, default);
|
||||
|
||||
// TODO Ask is normally a higher number than Bid!! Let's check this!! Maybe a Unit Test?
|
||||
@ -574,6 +529,93 @@ namespace BTCPayServer.Controllers
|
||||
return Request.GetRelativePathOrAbsolute(res);
|
||||
}
|
||||
|
||||
[HttpPost("/stores/{storeId}/custodian-accounts/{accountId}/withdraw/simulate")]
|
||||
public async Task<IActionResult> SimulateWithdrawJson(string storeId, string accountId,
|
||||
[FromBody] WithdrawRequestData withdrawRequestData)
|
||||
{
|
||||
if (string.IsNullOrEmpty(withdrawRequestData.PaymentMethod))
|
||||
{
|
||||
return BadRequest();
|
||||
}
|
||||
|
||||
var custodianAccount = await _custodianAccountRepository.FindById(storeId, accountId);
|
||||
|
||||
if (custodianAccount == null)
|
||||
return NotFound();
|
||||
|
||||
var custodian = _custodianRegistry.GetCustodianByCode(custodianAccount.CustodianCode);
|
||||
if (custodian == null)
|
||||
{
|
||||
// TODO The custodian account is broken. The custodian is no longer available. Maybe delete the custodian account?
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var vm = new WithdrawalPrepareViewModel();
|
||||
|
||||
try
|
||||
{
|
||||
if (custodian is ICanWithdraw withdrawableCustodian)
|
||||
{
|
||||
var config = custodianAccount.GetBlob();
|
||||
|
||||
try
|
||||
{
|
||||
var simulateWithdrawal =
|
||||
await _btcPayServerClient.SimulateCustodianAccountWithdrawal(storeId, accountId, withdrawRequestData,
|
||||
default);
|
||||
vm = new WithdrawalPrepareViewModel(simulateWithdrawal);
|
||||
|
||||
// There are no bad config fields, so we need an empty array
|
||||
vm.BadConfigFields = Array.Empty<string>();
|
||||
}
|
||||
catch (BadConfigException e)
|
||||
{
|
||||
Form configForm = await custodian.GetConfigForm();
|
||||
configForm.SetValues(config);
|
||||
string[] badConfigFields = new string[e.BadConfigKeys.Length];
|
||||
int i = 0;
|
||||
foreach (var oneField in configForm.GetAllFields())
|
||||
{
|
||||
foreach (var badConfigKey in e.BadConfigKeys)
|
||||
{
|
||||
if (oneField.FullName.Equals(badConfigKey))
|
||||
{
|
||||
var field = configForm.GetFieldByFullName(oneField.FullName);
|
||||
badConfigFields[i] = field.Label;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vm.BadConfigFields = badConfigFields;
|
||||
return Ok(vm);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
vm.ErrorMessage = e.Message;
|
||||
}
|
||||
|
||||
return Ok(vm);
|
||||
}
|
||||
|
||||
[HttpPost("/stores/{storeId}/custodian-accounts/{accountId}/withdraw")]
|
||||
public async Task<IActionResult> Withdraw(string storeId, string accountId,
|
||||
[FromBody] WithdrawRequestData request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await _btcPayServerClient.CreateCustodianAccountWithdrawal(storeId, accountId, request);
|
||||
return Ok(result);
|
||||
}
|
||||
catch (GreenfieldAPIException e)
|
||||
{
|
||||
var result = new ObjectResult(e.APIError) { StatusCode = e.HttpCode };
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
private StoreData GetCurrentStore() => HttpContext.GetStoreData();
|
||||
}
|
||||
}
|
||||
|
@ -222,7 +222,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
public RedirectToActionResult RedirectToStore(StoreData store)
|
||||
{
|
||||
return store.Role == StoreRoles.Owner
|
||||
return store.HasPermission(Policies.CanModifyStoreSettings)
|
||||
? RedirectToAction("Dashboard", "UIStores", new { storeId = store.Id })
|
||||
: RedirectToAction("ListInvoices", "UIInvoice", new { storeId = store.Id });
|
||||
}
|
||||
|
@ -80,15 +80,13 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
return UnprocessableEntity(new
|
||||
{
|
||||
ErrorMessage = response.ErrorDetail,
|
||||
AmountRemaining = invoice.Price
|
||||
ErrorMessage = response.ErrorDetail
|
||||
});
|
||||
|
||||
default:
|
||||
return UnprocessableEntity(new
|
||||
{
|
||||
ErrorMessage = $"Payment method {paymentMethodId} is not supported",
|
||||
AmountRemaining = invoice.Price
|
||||
ErrorMessage = $"Payment method {paymentMethodId} is not supported"
|
||||
});
|
||||
}
|
||||
|
||||
@ -97,8 +95,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
return BadRequest(new
|
||||
{
|
||||
ErrorMessage = e.Message,
|
||||
AmountRemaining = invoice.Price
|
||||
ErrorMessage = e.Message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -19,7 +19,9 @@ using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.InvoicingModels;
|
||||
using BTCPayServer.Models.PaymentRequestViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Bitcoin;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Invoices.Export;
|
||||
@ -137,10 +139,10 @@ namespace BTCPayServer.Controllers
|
||||
CreatedDate = invoice.InvoiceTime,
|
||||
ExpirationDate = invoice.ExpirationTime,
|
||||
MonitoringDate = invoice.MonitoringExpiration,
|
||||
Fiat = _CurrencyNameTable.DisplayFormatCurrency(invoice.Price, invoice.Currency),
|
||||
Fiat = _displayFormatter.Currency(invoice.Price, invoice.Currency),
|
||||
TaxIncluded = invoice.Metadata.TaxIncluded is null
|
||||
? null
|
||||
: _CurrencyNameTable.DisplayFormatCurrency(invoice.Metadata.TaxIncluded ?? 0.0m, invoice.Currency),
|
||||
: _displayFormatter.Currency(invoice.Metadata.TaxIncluded ?? 0.0m, invoice.Currency),
|
||||
NotificationUrl = invoice.NotificationURL?.AbsoluteUri,
|
||||
RedirectUrl = invoice.RedirectURL?.AbsoluteUri,
|
||||
TypedMetadata = invoice.Metadata,
|
||||
@ -229,12 +231,14 @@ namespace BTCPayServer.Controllers
|
||||
Amount = amount,
|
||||
Paid = paid,
|
||||
ReceivedDate = paymentEntity.ReceivedTime.DateTime,
|
||||
PaidFormatted = _CurrencyNameTable.FormatCurrency(paid, i.Currency),
|
||||
RateFormatted = _CurrencyNameTable.FormatCurrency(rate, i.Currency),
|
||||
PaidFormatted = _displayFormatter.Currency(paid, i.Currency, DisplayFormatter.CurrencyFormat.Symbol),
|
||||
RateFormatted = _displayFormatter.Currency(rate, i.Currency, DisplayFormatter.CurrencyFormat.Symbol),
|
||||
PaymentMethod = paymentMethodId.ToPrettyString(),
|
||||
Link = link,
|
||||
Id = txId,
|
||||
Destination = paymentData.GetDestination()
|
||||
Destination = paymentData.GetDestination(),
|
||||
PaymentProof = GetPaymentProof(paymentData),
|
||||
PaymentType = paymentData.GetPaymentType()
|
||||
};
|
||||
})
|
||||
.Where(payment => payment != null)
|
||||
@ -246,6 +250,17 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
private string? GetPaymentProof(CryptoPaymentData paymentData)
|
||||
{
|
||||
return paymentData switch
|
||||
{
|
||||
BitcoinLikePaymentData b => b.Outpoint.ToString(),
|
||||
LightningPaymentData l => l.Preimage,
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
private string? GetTransactionLink(PaymentMethodId paymentMethodId, string txId)
|
||||
{
|
||||
var network = _NetworkProvider.GetNetwork(paymentMethodId.CryptoCode);
|
||||
@ -253,7 +268,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpGet("invoices/{invoiceId}/refund")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[Authorize(Policy = Policies.CanCreateNonApprovedPullPayments, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> Refund([FromServices] IEnumerable<IPayoutHandler> payoutHandlers, string invoiceId, CancellationToken cancellationToken)
|
||||
{
|
||||
await using var ctx = _dbContextFactory.CreateContext();
|
||||
@ -316,7 +331,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpPost("invoices/{invoiceId}/refund")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[Authorize(Policy = Policies.CanCreateNonApprovedPullPayments, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> Refund(string invoiceId, RefundModel model, CancellationToken cancellationToken)
|
||||
{
|
||||
await using var ctx = _dbContextFactory.CreateContext();
|
||||
@ -354,8 +369,7 @@ namespace BTCPayServer.Controllers
|
||||
var cryptoPaid = paymentMethod.Calculate().Paid.ToDecimal(MoneyUnit.BTC);
|
||||
var paidCurrency = Math.Round(cryptoPaid * paymentMethod.Rate, cdCurrency.Divisibility);
|
||||
model.CryptoAmountThen = cryptoPaid.RoundToSignificant(paymentMethodDivisibility);
|
||||
model.RateThenText =
|
||||
_CurrencyNameTable.DisplayFormatCurrency(model.CryptoAmountThen, paymentMethodId.CryptoCode);
|
||||
model.RateThenText = _displayFormatter.Currency(model.CryptoAmountThen, paymentMethodId.CryptoCode);
|
||||
rules = store.GetStoreBlob().GetRateRules(_NetworkProvider);
|
||||
rateResult = await _RateProvider.FetchRate(
|
||||
new CurrencyPair(paymentMethodId.CryptoCode, invoice.Currency), rules,
|
||||
@ -369,13 +383,12 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
model.CryptoAmountNow = Math.Round(paidCurrency / rateResult.BidAsk.Bid, paymentMethodDivisibility);
|
||||
model.CurrentRateText =
|
||||
_CurrencyNameTable.DisplayFormatCurrency(model.CryptoAmountNow, paymentMethodId.CryptoCode);
|
||||
model.CurrentRateText = _displayFormatter.Currency(model.CryptoAmountNow, paymentMethodId.CryptoCode);
|
||||
model.FiatAmount = paidCurrency;
|
||||
}
|
||||
model.CustomAmount = model.FiatAmount;
|
||||
model.CustomCurrency = invoice.Currency;
|
||||
model.FiatText = _CurrencyNameTable.DisplayFormatCurrency(model.FiatAmount, invoice.Currency);
|
||||
model.FiatText = _displayFormatter.Currency(model.FiatAmount, invoice.Currency);
|
||||
return View("_RefundModal", model);
|
||||
|
||||
case RefundSteps.SelectRate:
|
||||
@ -386,18 +399,21 @@ namespace BTCPayServer.Controllers
|
||||
StoreId = invoice.StoreId,
|
||||
BOLT11Expiration = store.GetStoreBlob().RefundBOLT11Expiration
|
||||
};
|
||||
var authorizedForAutoApprove = (await
|
||||
_authorizationService.AuthorizeAsync(User, invoice.StoreId, Policies.CanCreatePullPayments))
|
||||
.Succeeded;
|
||||
switch (model.SelectedRefundOption)
|
||||
{
|
||||
case "RateThen":
|
||||
createPullPayment.Currency = paymentMethodId.CryptoCode;
|
||||
createPullPayment.Amount = model.CryptoAmountThen;
|
||||
createPullPayment.AutoApproveClaims = true;
|
||||
createPullPayment.AutoApproveClaims = authorizedForAutoApprove;
|
||||
break;
|
||||
|
||||
case "CurrentRate":
|
||||
createPullPayment.Currency = paymentMethodId.CryptoCode;
|
||||
createPullPayment.Amount = model.CryptoAmountNow;
|
||||
createPullPayment.AutoApproveClaims = true;
|
||||
createPullPayment.AutoApproveClaims = authorizedForAutoApprove;
|
||||
break;
|
||||
|
||||
case "Fiat":
|
||||
@ -442,7 +458,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
createPullPayment.Currency = model.CustomCurrency;
|
||||
createPullPayment.Amount = model.CustomAmount;
|
||||
createPullPayment.AutoApproveClaims = paymentMethodId.CryptoCode == model.CustomCurrency;
|
||||
createPullPayment.AutoApproveClaims = authorizedForAutoApprove && paymentMethodId.CryptoCode == model.CustomCurrency;
|
||||
break;
|
||||
|
||||
default:
|
||||
@ -477,7 +493,6 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
private InvoiceDetailsModel InvoicePopulatePayments(InvoiceEntity invoice)
|
||||
{
|
||||
|
||||
var overpaid = false;
|
||||
var model = new InvoiceDetailsModel
|
||||
{
|
||||
@ -500,15 +515,11 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
PaymentMethodId = paymentMethodId,
|
||||
PaymentMethod = paymentMethodId.ToPrettyString(),
|
||||
Due = _CurrencyNameTable.DisplayFormatCurrency(accounting.Due.ToDecimal(MoneyUnit.BTC),
|
||||
paymentMethodId.CryptoCode),
|
||||
Paid = _CurrencyNameTable.DisplayFormatCurrency(
|
||||
accounting.CryptoPaid.ToDecimal(MoneyUnit.BTC),
|
||||
paymentMethodId.CryptoCode),
|
||||
Overpaid = _CurrencyNameTable.DisplayFormatCurrency(
|
||||
overpaidAmount, paymentMethodId.CryptoCode),
|
||||
Due = _displayFormatter.Currency(accounting.Due.ToDecimal(MoneyUnit.BTC), paymentMethodId.CryptoCode),
|
||||
Paid = _displayFormatter.Currency(accounting.CryptoPaid.ToDecimal(MoneyUnit.BTC), paymentMethodId.CryptoCode),
|
||||
Overpaid = _displayFormatter.Currency(overpaidAmount, paymentMethodId.CryptoCode),
|
||||
Address = data.GetPaymentMethodDetails().GetPaymentDestination(),
|
||||
Rate = ExchangeRate(data),
|
||||
Rate = ExchangeRate(data.GetId().CryptoCode, data),
|
||||
PaymentMethodRaw = data
|
||||
};
|
||||
}).ToList()
|
||||
@ -789,14 +800,15 @@ namespace BTCPayServer.Controllers
|
||||
BrandColor = storeBlob.BrandColor,
|
||||
CheckoutType = invoice.CheckoutType ?? storeBlob.CheckoutType,
|
||||
HtmlTitle = storeBlob.HtmlTitle ?? "BTCPay Invoice",
|
||||
CelebratePayment = storeBlob.CelebratePayment,
|
||||
OnChainWithLnInvoiceFallback = storeBlob.OnChainWithLnInvoiceFallback,
|
||||
CryptoImage = Request.GetRelativePathOrAbsolute(paymentMethodHandler.GetCryptoImage(paymentMethodId)),
|
||||
BtcAddress = paymentMethodDetails.GetPaymentDestination(),
|
||||
BtcDue = accounting.Due.ShowMoney(divisibility),
|
||||
BtcPaid = accounting.Paid.ShowMoney(divisibility),
|
||||
InvoiceCurrency = invoice.Currency,
|
||||
OrderAmount = (accounting.TotalDue - accounting.NetworkFee).ShowMoney(divisibility),
|
||||
IsUnsetTopUp = invoice.IsUnsetTopUp(),
|
||||
OrderAmountFiat = OrderAmountFromInvoice(network.CryptoCode, invoice),
|
||||
CustomerEmail = invoice.RefundMail,
|
||||
RequiresRefundEmail = invoice.RequiresRefundEmail ?? storeBlob.RequiresRefundEmail,
|
||||
ExpirationSeconds = Math.Max(0, (int)(invoice.ExpirationTime - DateTimeOffset.UtcNow).TotalSeconds),
|
||||
@ -804,7 +816,7 @@ namespace BTCPayServer.Controllers
|
||||
MaxTimeSeconds = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalSeconds,
|
||||
MaxTimeMinutes = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalMinutes,
|
||||
ItemDesc = invoice.Metadata.ItemDesc,
|
||||
Rate = ExchangeRate(paymentMethod),
|
||||
Rate = ExchangeRate(network.CryptoCode, paymentMethod, DisplayFormatter.CurrencyFormat.Symbol),
|
||||
MerchantRefLink = invoice.RedirectURL?.AbsoluteUri ?? receiptUrl ?? "/",
|
||||
ReceiptLink = receiptUrl,
|
||||
RedirectAutomatically = invoice.RedirectAutomatically,
|
||||
@ -817,7 +829,15 @@ namespace BTCPayServer.Controllers
|
||||
NetworkFeeMode.Never => 0,
|
||||
_ => throw new NotImplementedException()
|
||||
},
|
||||
BtcPaid = accounting.Paid.ShowMoney(divisibility),
|
||||
RequiredConfirmations = invoice.SpeedPolicy switch
|
||||
{
|
||||
SpeedPolicy.HighSpeed => 0,
|
||||
SpeedPolicy.MediumSpeed => 1,
|
||||
SpeedPolicy.LowMediumSpeed => 2,
|
||||
SpeedPolicy.LowSpeed => 6,
|
||||
_ => null
|
||||
},
|
||||
ReceivedConfirmations = invoice.GetAllBitcoinPaymentData(false).FirstOrDefault()?.ConfirmationCount,
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
Status = invoice.StatusString,
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
@ -864,23 +884,33 @@ namespace BTCPayServer.Controllers
|
||||
model.UISettings = paymentMethodHandler.GetCheckoutUISettings();
|
||||
model.PaymentMethodId = paymentMethodId.ToString();
|
||||
model.PaymentType = paymentMethodId.PaymentType.ToString();
|
||||
model.OrderAmountFiat = OrderAmountFromInvoice(model.CryptoCode, invoice, DisplayFormatter.CurrencyFormat.Symbol);
|
||||
var expiration = TimeSpan.FromSeconds(model.ExpirationSeconds);
|
||||
model.TimeLeft = expiration.PrettyPrint();
|
||||
return model;
|
||||
}
|
||||
|
||||
private string? OrderAmountFromInvoice(string cryptoCode, InvoiceEntity invoiceEntity)
|
||||
private string? OrderAmountFromInvoice(string cryptoCode, InvoiceEntity invoiceEntity, DisplayFormatter.CurrencyFormat format = DisplayFormatter.CurrencyFormat.Code)
|
||||
{
|
||||
var currency = invoiceEntity.Currency;
|
||||
var crypto = cryptoCode.ToUpperInvariant(); // uppercase to make comparison easier, might be "sats"
|
||||
|
||||
// if invoice source currency is the same as currently display currency, no need for "order amount from invoice"
|
||||
if (cryptoCode == invoiceEntity.Currency)
|
||||
if (crypto == currency || (crypto == "SATS" && currency == "BTC") || (crypto == "BTC" && currency == "SATS"))
|
||||
return null;
|
||||
|
||||
return _CurrencyNameTable.DisplayFormatCurrency(invoiceEntity.Price, invoiceEntity.Currency);
|
||||
return _displayFormatter.Currency(invoiceEntity.Price, currency, format);
|
||||
}
|
||||
private string ExchangeRate(PaymentMethod paymentMethod)
|
||||
|
||||
private string? ExchangeRate(string cryptoCode, PaymentMethod paymentMethod, DisplayFormatter.CurrencyFormat format = DisplayFormatter.CurrencyFormat.Code)
|
||||
{
|
||||
string currency = paymentMethod.ParentEntity.Currency;
|
||||
return _CurrencyNameTable.DisplayFormatCurrency(paymentMethod.Rate, currency);
|
||||
var currency = paymentMethod.ParentEntity.Currency;
|
||||
var crypto = cryptoCode.ToUpperInvariant(); // uppercase to make comparison easier, might be "sats"
|
||||
|
||||
if (crypto == currency || (crypto == "SATS" && currency == "BTC") || (crypto == "BTC" && currency == "SATS"))
|
||||
return null;
|
||||
|
||||
return _displayFormatter.Currency(paymentMethod.Rate, currency, format);
|
||||
}
|
||||
|
||||
[HttpGet("i/{invoiceId}/status")]
|
||||
@ -1003,7 +1033,8 @@ namespace BTCPayServer.Controllers
|
||||
InvoiceId = invoice.Id,
|
||||
OrderId = invoice.Metadata.OrderId ?? string.Empty,
|
||||
RedirectUrl = invoice.RedirectURL?.AbsoluteUri ?? string.Empty,
|
||||
AmountCurrency = _CurrencyNameTable.DisplayFormatCurrency(invoice.Price, invoice.Currency),
|
||||
Amount = invoice.Price,
|
||||
Currency = invoice.Currency,
|
||||
CanMarkInvalid = state.CanMarkInvalid(),
|
||||
CanMarkSettled = state.CanMarkComplete(),
|
||||
Details = InvoicePopulatePayments(invoice),
|
||||
|
@ -24,6 +24,7 @@ using BTCPayServer.Services.PaymentRequests;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Validation;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@ -45,6 +46,7 @@ namespace BTCPayServer.Controllers
|
||||
readonly StoreRepository _StoreRepository;
|
||||
readonly UserManager<ApplicationUser> _UserManager;
|
||||
private readonly CurrencyNameTable _CurrencyNameTable;
|
||||
private readonly DisplayFormatter _displayFormatter;
|
||||
readonly EventAggregator _EventAggregator;
|
||||
readonly BTCPayNetworkProvider _NetworkProvider;
|
||||
private readonly PaymentMethodHandlerDictionary _paymentMethodHandlerDictionary;
|
||||
@ -55,12 +57,14 @@ namespace BTCPayServer.Controllers
|
||||
private readonly UIWalletsController _walletsController;
|
||||
private readonly InvoiceActivator _invoiceActivator;
|
||||
private readonly LinkGenerator _linkGenerator;
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
|
||||
public WebhookSender WebhookNotificationManager { get; }
|
||||
|
||||
public UIInvoiceController(
|
||||
InvoiceRepository invoiceRepository,
|
||||
WalletRepository walletRepository,
|
||||
DisplayFormatter displayFormatter,
|
||||
CurrencyNameTable currencyNameTable,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
RateFetcher rateProvider,
|
||||
@ -76,8 +80,10 @@ namespace BTCPayServer.Controllers
|
||||
ExplorerClientProvider explorerClients,
|
||||
UIWalletsController walletsController,
|
||||
InvoiceActivator invoiceActivator,
|
||||
LinkGenerator linkGenerator)
|
||||
LinkGenerator linkGenerator,
|
||||
IAuthorizationService authorizationService)
|
||||
{
|
||||
_displayFormatter = displayFormatter;
|
||||
_CurrencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable));
|
||||
_StoreRepository = storeRepository ?? throw new ArgumentNullException(nameof(storeRepository));
|
||||
_InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository));
|
||||
@ -95,6 +101,7 @@ namespace BTCPayServer.Controllers
|
||||
_walletsController = walletsController;
|
||||
_invoiceActivator = invoiceActivator;
|
||||
_linkGenerator = linkGenerator;
|
||||
_authorizationService = authorizationService;
|
||||
}
|
||||
|
||||
|
||||
@ -210,7 +217,8 @@ namespace BTCPayServer.Controllers
|
||||
return await CreateInvoiceCoreRaw(invoiceRequest, storeData, request.GetAbsoluteRoot(), additionalTags, cancellationToken);
|
||||
}
|
||||
|
||||
internal async Task<InvoiceEntity> CreateInvoiceCoreRaw(CreateInvoiceRequest invoice, StoreData store, string serverUrl, List<string>? additionalTags = null, CancellationToken cancellationToken = default, Action<InvoiceEntity>? entityManipulator = null)
|
||||
[NonAction]
|
||||
public async Task<InvoiceEntity> CreateInvoiceCoreRaw(CreateInvoiceRequest invoice, StoreData store, string serverUrl, List<string>? additionalTags = null, CancellationToken cancellationToken = default, Action<InvoiceEntity>? entityManipulator = null)
|
||||
{
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var entity = _InvoiceRepository.CreateNewInvoice();
|
||||
|
@ -6,6 +6,7 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Client;
|
||||
@ -18,6 +19,8 @@ using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Plugins.Crowdfund;
|
||||
using BTCPayServer.Plugins.PointOfSale;
|
||||
using BTCPayServer.Plugins.PointOfSale.Models;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Apps;
|
||||
@ -47,13 +50,13 @@ namespace BTCPayServer
|
||||
private readonly LightningLikePaymentHandler _lightningLikePaymentHandler;
|
||||
private readonly StoreRepository _storeRepository;
|
||||
private readonly AppService _appService;
|
||||
|
||||
private readonly UIInvoiceController _invoiceController;
|
||||
private readonly LinkGenerator _linkGenerator;
|
||||
private readonly LightningAddressService _lightningAddressService;
|
||||
private readonly LightningLikePayoutHandler _lightningLikePayoutHandler;
|
||||
private readonly PullPaymentHostedService _pullPaymentHostedService;
|
||||
private readonly BTCPayNetworkJsonSerializerSettings _btcPayNetworkJsonSerializerSettings;
|
||||
private readonly IPluginHookService _pluginHookService;
|
||||
|
||||
public UILNURLController(InvoiceRepository invoiceRepository,
|
||||
EventAggregator eventAggregator,
|
||||
@ -66,7 +69,8 @@ namespace BTCPayServer
|
||||
LightningAddressService lightningAddressService,
|
||||
LightningLikePayoutHandler lightningLikePayoutHandler,
|
||||
PullPaymentHostedService pullPaymentHostedService,
|
||||
BTCPayNetworkJsonSerializerSettings btcPayNetworkJsonSerializerSettings)
|
||||
BTCPayNetworkJsonSerializerSettings btcPayNetworkJsonSerializerSettings,
|
||||
IPluginHookService pluginHookService)
|
||||
{
|
||||
_invoiceRepository = invoiceRepository;
|
||||
_eventAggregator = eventAggregator;
|
||||
@ -80,6 +84,7 @@ namespace BTCPayServer
|
||||
_lightningLikePayoutHandler = lightningLikePayoutHandler;
|
||||
_pullPaymentHostedService = pullPaymentHostedService;
|
||||
_btcPayNetworkJsonSerializerSettings = btcPayNetworkJsonSerializerSettings;
|
||||
_pluginHookService = pluginHookService;
|
||||
}
|
||||
|
||||
[HttpGet("withdraw/pp/{pullPaymentId}")]
|
||||
@ -155,6 +160,7 @@ namespace BTCPayServer
|
||||
|
||||
if (claimResponse.Result != ClaimRequest.ClaimResult.Ok)
|
||||
return BadRequest(new LNUrlStatusResponse { Status = "ERROR", Reason = "Payment request could not be paid" });
|
||||
|
||||
switch (claimResponse.PayoutData.State)
|
||||
{
|
||||
case PayoutState.AwaitingPayment:
|
||||
@ -249,37 +255,49 @@ namespace BTCPayServer
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
ViewPointOfSaleViewModel.Item[] items = null;
|
||||
string currencyCode = null;
|
||||
ViewPointOfSaleViewModel.Item[] items;
|
||||
string currencyCode;
|
||||
PointOfSaleSettings posS = null;
|
||||
switch (app.AppType)
|
||||
{
|
||||
case nameof(AppType.Crowdfund):
|
||||
case CrowdfundAppType.AppType:
|
||||
var cfS = app.GetSettings<CrowdfundSettings>();
|
||||
currencyCode = cfS.TargetCurrency;
|
||||
items = _appService.Parse(cfS.PerksTemplate, cfS.TargetCurrency);
|
||||
break;
|
||||
case nameof(AppType.PointOfSale):
|
||||
var posS = app.GetSettings<PointOfSaleSettings>();
|
||||
case PointOfSaleAppType.AppType:
|
||||
posS = app.GetSettings<PointOfSaleSettings>();
|
||||
currencyCode = posS.Currency;
|
||||
items = _appService.Parse(posS.Template, posS.Currency);
|
||||
break;
|
||||
default:
|
||||
//TODO: Allow other apps to define lnurl support
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var escapedItemId = Extensions.UnescapeBackSlashUriString(itemCode);
|
||||
var item = items.FirstOrDefault(item1 =>
|
||||
item1.Id.Equals(itemCode, StringComparison.InvariantCultureIgnoreCase) ||
|
||||
item1.Id.Equals(escapedItemId, StringComparison.InvariantCultureIgnoreCase));
|
||||
ViewPointOfSaleViewModel.Item item = null;
|
||||
if (!string.IsNullOrEmpty(itemCode))
|
||||
{
|
||||
var escapedItemId = Extensions.UnescapeBackSlashUriString(itemCode);
|
||||
item = items.FirstOrDefault(item1 =>
|
||||
item1.Id.Equals(itemCode, StringComparison.InvariantCultureIgnoreCase) ||
|
||||
item1.Id.Equals(escapedItemId, StringComparison.InvariantCultureIgnoreCase));
|
||||
|
||||
if (item is null ||
|
||||
item.Inventory <= 0 ||
|
||||
(item.PaymentMethods?.Any() is true &&
|
||||
item.PaymentMethods?.Any(s => PaymentMethodId.Parse(s) == pmi) is false))
|
||||
if (item is null ||
|
||||
item.Inventory <= 0 ||
|
||||
(item.PaymentMethods?.Any() is true &&
|
||||
item.PaymentMethods?.Any(s => PaymentMethodId.Parse(s) == pmi) is false))
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
}
|
||||
else if (app.AppType == PointOfSaleAppType.AppType && posS?.ShowCustomAmount is not true)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
|
||||
return await GetLNURL(cryptoCode, app.StoreDataId, currencyCode, null, null,
|
||||
() => (null, app, item, new List<string> { AppService.GetAppInternalTag(appId) }, item.Price.Value, true));
|
||||
() => (null, app, item, new List<string> { AppService.GetAppInternalTag(appId) }, item?.Price.Value, true));
|
||||
}
|
||||
|
||||
public class EditLightningAddressVM
|
||||
@ -311,11 +329,8 @@ namespace BTCPayServer
|
||||
public decimal? Max { get; set; }
|
||||
}
|
||||
|
||||
public ConcurrentDictionary<string, LightningAddressItem> Items { get; set; } =
|
||||
new ConcurrentDictionary<string, LightningAddressItem>();
|
||||
|
||||
public ConcurrentDictionary<string, string[]> StoreToItemMap { get; set; } =
|
||||
new ConcurrentDictionary<string, string[]>();
|
||||
public ConcurrentDictionary<string, LightningAddressItem> Items { get; } = new ();
|
||||
public ConcurrentDictionary<string, string[]> StoreToItemMap { get; } = new ();
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
@ -389,7 +404,7 @@ namespace BTCPayServer
|
||||
|
||||
var redirectUrl = app?.AppType switch
|
||||
{
|
||||
nameof(AppType.PointOfSale) => app.GetSettings<PointOfSaleSettings>().RedirectUrl ??
|
||||
PointOfSaleAppType.AppType => app.GetSettings<PointOfSaleSettings>().RedirectUrl ??
|
||||
HttpContext.Request.GetAbsoluteUri($"/apps/{app.Id}/pos"),
|
||||
_ => null
|
||||
};
|
||||
@ -442,31 +457,36 @@ namespace BTCPayServer
|
||||
await _invoiceRepository.UpdateInvoicePaymentMethod(i.Id, pm);
|
||||
}
|
||||
|
||||
var description = blob.LightningDescriptionTemplate
|
||||
var invoiceDescription = blob.LightningDescriptionTemplate
|
||||
.Replace("{StoreName}", store.StoreName ?? "", StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("{ItemDescription}", i.Metadata.ItemDesc ?? "", StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("{OrderId}", i.Metadata.OrderId ?? "", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
lnurlMetadata.Add(new[] { "text/plain", description });
|
||||
lnurlMetadata.Add(new[] { "text/plain", invoiceDescription });
|
||||
if (!string.IsNullOrEmpty(username))
|
||||
{
|
||||
lnurlMetadata.Add(new[] { "text/identifier", lnAddress });
|
||||
}
|
||||
return Ok(new LNURLPayRequest
|
||||
|
||||
if (await _pluginHookService.ApplyFilter("modify-lnurlp-request", new LNURLPayRequest
|
||||
{
|
||||
Tag = "payRequest",
|
||||
MinSendable = new LightMoney(min ?? 1m, LightMoneyUnit.Satoshi),
|
||||
MaxSendable =
|
||||
max is null
|
||||
? LightMoney.FromUnit(6.12m, LightMoneyUnit.BTC)
|
||||
: new LightMoney(max.Value, LightMoneyUnit.Satoshi),
|
||||
CommentAllowed = lnUrlMethod.LUD12Enabled ? 2000 : 0,
|
||||
Metadata = JsonConvert.SerializeObject(lnurlMetadata),
|
||||
Callback = new Uri(_linkGenerator.GetUriByAction(
|
||||
action: nameof(GetLNURLForInvoice),
|
||||
controller: "UILNURL",
|
||||
values: new {cryptoCode, invoiceId = i.Id}, Request.Scheme, Request.Host, Request.PathBase))
|
||||
}) is not LNURLPayRequest lnurlp)
|
||||
{
|
||||
Tag = "payRequest",
|
||||
MinSendable = new LightMoney(min ?? 1m, LightMoneyUnit.Satoshi),
|
||||
MaxSendable =
|
||||
max is null
|
||||
? LightMoney.FromUnit(6.12m, LightMoneyUnit.BTC)
|
||||
: new LightMoney(max.Value, LightMoneyUnit.Satoshi),
|
||||
CommentAllowed = lnUrlMethod.LUD12Enabled ? 2000 : 0,
|
||||
Metadata = JsonConvert.SerializeObject(lnurlMetadata),
|
||||
Callback = new Uri(_linkGenerator.GetUriByAction(
|
||||
action: nameof(GetLNURLForInvoice),
|
||||
controller: "UILNURL",
|
||||
values: new { cryptoCode, invoiceId = i.Id }, Request.Scheme, Request.Host, Request.PathBase))
|
||||
});
|
||||
return NotFound();
|
||||
}
|
||||
return Ok(lnurlp);
|
||||
}
|
||||
|
||||
[HttpGet("pay/i/{invoiceId}")]
|
||||
@ -519,12 +539,12 @@ namespace BTCPayServer
|
||||
List<string[]> lnurlMetadata = new();
|
||||
|
||||
var blob = store.GetStoreBlob();
|
||||
var description = blob.LightningDescriptionTemplate
|
||||
var invoiceDescription = blob.LightningDescriptionTemplate
|
||||
.Replace("{StoreName}", store.StoreName ?? "", StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("{ItemDescription}", i.Metadata.ItemDesc ?? "", StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("{OrderId}", i.Metadata.OrderId ?? "", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
lnurlMetadata.Add(new[] { "text/plain", description });
|
||||
lnurlMetadata.Add(new[] { "text/plain", invoiceDescription });
|
||||
if (!string.IsNullOrEmpty(paymentMethodDetails.ConsumedLightningAddress))
|
||||
{
|
||||
lnurlMetadata.Add(new[] { "text/identifier", paymentMethodDetails.ConsumedLightningAddress });
|
||||
@ -556,15 +576,20 @@ namespace BTCPayServer
|
||||
|
||||
if (amt is null)
|
||||
{
|
||||
return Ok(new LNURLPayRequest
|
||||
if (await _pluginHookService.ApplyFilter("modify-lnurlp-request", new LNURLPayRequest
|
||||
{
|
||||
Tag = "payRequest",
|
||||
MinSendable = min,
|
||||
MaxSendable = max,
|
||||
CommentAllowed = lnurlSupportedPaymentMethod.LUD12Enabled ? 2000 : 0,
|
||||
Metadata = metadata,
|
||||
Callback = new Uri(Request.GetCurrentUrl())
|
||||
}) is not LNURLPayRequest lnurlp)
|
||||
{
|
||||
Tag = "payRequest",
|
||||
MinSendable = min,
|
||||
MaxSendable = max,
|
||||
CommentAllowed = lnurlSupportedPaymentMethod.LUD12Enabled ? 2000 : 0,
|
||||
Metadata = metadata,
|
||||
Callback = new Uri(Request.GetCurrentUrl())
|
||||
});
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return Ok(lnurlp);
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(paymentMethodDetails.BOLT11) || paymentMethodDetails.GeneratedBoltAmount != amt)
|
||||
@ -588,14 +613,19 @@ namespace BTCPayServer
|
||||
try
|
||||
{
|
||||
var expiry = i.ExpirationTime.ToUniversalTime() - DateTimeOffset.UtcNow;
|
||||
var param = new CreateInvoiceParams(amt, metadata, expiry)
|
||||
var description = (await _pluginHookService.ApplyFilter("modify-lnurlp-description", metadata)) as string;
|
||||
if (description is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
var param = new CreateInvoiceParams(amt, description, expiry)
|
||||
{
|
||||
PrivateRouteHints = blob.LightningPrivateRouteHints,
|
||||
DescriptionHashOnly = true
|
||||
};
|
||||
invoice = await client.CreateInvoice(param);
|
||||
if (!BOLT11PaymentRequest.Parse(invoice.BOLT11, network.NBitcoinNetwork)
|
||||
.VerifyDescriptionHash(metadata))
|
||||
.VerifyDescriptionHash(description))
|
||||
{
|
||||
return BadRequest(new LNUrlStatusResponse
|
||||
{
|
||||
|
@ -14,6 +14,7 @@ using BTCPayServer.Forms.Models;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.PaymentRequestViewModels;
|
||||
using BTCPayServer.PaymentRequest;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.PaymentRequests;
|
||||
using BTCPayServer.Services.Rates;
|
||||
@ -37,6 +38,7 @@ namespace BTCPayServer.Controllers
|
||||
private readonly PaymentRequestService _PaymentRequestService;
|
||||
private readonly EventAggregator _EventAggregator;
|
||||
private readonly CurrencyNameTable _Currencies;
|
||||
private readonly DisplayFormatter _displayFormatter;
|
||||
private readonly InvoiceRepository _InvoiceRepository;
|
||||
private readonly StoreRepository _storeRepository;
|
||||
|
||||
@ -50,6 +52,7 @@ namespace BTCPayServer.Controllers
|
||||
PaymentRequestService paymentRequestService,
|
||||
EventAggregator eventAggregator,
|
||||
CurrencyNameTable currencies,
|
||||
DisplayFormatter displayFormatter,
|
||||
StoreRepository storeRepository,
|
||||
InvoiceRepository invoiceRepository,
|
||||
FormComponentProviders formProviders,
|
||||
@ -61,6 +64,7 @@ namespace BTCPayServer.Controllers
|
||||
_PaymentRequestService = paymentRequestService;
|
||||
_EventAggregator = eventAggregator;
|
||||
_Currencies = currencies;
|
||||
_displayFormatter = displayFormatter;
|
||||
_storeRepository = storeRepository;
|
||||
_InvoiceRepository = invoiceRepository;
|
||||
FormProviders = formProviders;
|
||||
@ -89,7 +93,7 @@ namespace BTCPayServer.Controllers
|
||||
var blob = data.GetBlob();
|
||||
return new ViewPaymentRequestViewModel(data)
|
||||
{
|
||||
AmountFormatted = _Currencies.DisplayFormatCurrency(blob.Amount, blob.Currency)
|
||||
AmountFormatted = _displayFormatter.Currency(blob.Amount, blob.Currency)
|
||||
};
|
||||
}).ToList();
|
||||
|
||||
|
@ -5,7 +5,6 @@ using System.Web;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Plugins.PayButton.Models;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
|
@ -27,6 +27,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
private readonly ApplicationDbContextFactory _dbContextFactory;
|
||||
private readonly CurrencyNameTable _currencyNameTable;
|
||||
private readonly DisplayFormatter _displayFormatter;
|
||||
private readonly PullPaymentHostedService _pullPaymentHostedService;
|
||||
private readonly BTCPayNetworkJsonSerializerSettings _serializerSettings;
|
||||
private readonly IEnumerable<IPayoutHandler> _payoutHandlers;
|
||||
@ -34,6 +35,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
public UIPullPaymentController(ApplicationDbContextFactory dbContextFactory,
|
||||
CurrencyNameTable currencyNameTable,
|
||||
DisplayFormatter displayFormatter,
|
||||
PullPaymentHostedService pullPaymentHostedService,
|
||||
BTCPayNetworkJsonSerializerSettings serializerSettings,
|
||||
IEnumerable<IPayoutHandler> payoutHandlers,
|
||||
@ -41,6 +43,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_currencyNameTable = currencyNameTable;
|
||||
_displayFormatter = displayFormatter;
|
||||
_pullPaymentHostedService = pullPaymentHostedService;
|
||||
_serializerSettings = serializerSettings;
|
||||
_payoutHandlers = payoutHandlers;
|
||||
@ -79,12 +82,9 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
BrandColor = storeBlob.BrandColor,
|
||||
CssFileId = storeBlob.CssFileId,
|
||||
AmountFormatted = _currencyNameTable.FormatCurrency(blob.Limit, blob.Currency),
|
||||
AmountCollected = totalPaid,
|
||||
AmountCollectedFormatted = _currencyNameTable.FormatCurrency(totalPaid, blob.Currency),
|
||||
AmountDue = amountDue,
|
||||
ClaimedAmount = amountDue,
|
||||
AmountDueFormatted = _currencyNameTable.FormatCurrency(amountDue, blob.Currency),
|
||||
CurrencyData = cd,
|
||||
StartDate = pp.StartDate,
|
||||
LastRefreshed = DateTime.UtcNow,
|
||||
@ -93,7 +93,6 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
Id = entity.Entity.Id,
|
||||
Amount = entity.Blob.Amount,
|
||||
AmountFormatted = _currencyNameTable.FormatCurrency(entity.Blob.Amount, blob.Currency),
|
||||
Currency = blob.Currency,
|
||||
Status = entity.Entity.State,
|
||||
Destination = entity.Blob.Destination,
|
||||
@ -200,8 +199,8 @@ namespace BTCPayServer.Controllers
|
||||
var amount = ppBlob.Currency == "SATS" ? new Money(vm.ClaimedAmount, MoneyUnit.Satoshi).ToUnit(MoneyUnit.BTC) : vm.ClaimedAmount;
|
||||
if (destination.destination.Amount != null && amount != destination.destination.Amount)
|
||||
{
|
||||
var implied = _currencyNameTable.DisplayFormatCurrency(destination.destination.Amount.Value, paymentMethodId.CryptoCode);
|
||||
var provided = _currencyNameTable.DisplayFormatCurrency(vm.ClaimedAmount, ppBlob.Currency);
|
||||
var implied = _displayFormatter.Currency(destination.destination.Amount.Value, paymentMethodId.CryptoCode, DisplayFormatter.CurrencyFormat.Symbol);
|
||||
var provided = _displayFormatter.Currency(vm.ClaimedAmount, ppBlob.Currency, DisplayFormatter.CurrencyFormat.Symbol);
|
||||
ModelState.AddModelError(nameof(vm.ClaimedAmount),
|
||||
$"Amount implied in destination ({implied}) does not match the payout amount provided ({provided}).");
|
||||
}
|
||||
@ -235,7 +234,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Message = $"Your claim request of {_currencyNameTable.DisplayFormatCurrency(vm.ClaimedAmount, ppBlob.Currency)} to {vm.Destination} has been submitted and is awaiting {(result.PayoutData.State == PayoutState.AwaitingApproval ? "approval" : "payment")}.",
|
||||
Message = $"Your claim request of {_displayFormatter.Currency(vm.ClaimedAmount, ppBlob.Currency, DisplayFormatter.CurrencyFormat.Symbol)} to {vm.Destination} has been submitted and is awaiting {(result.PayoutData.State == PayoutState.AwaitingApproval ? "approval" : "payment")}.",
|
||||
Severity = StatusMessageModel.StatusSeverity.Success
|
||||
});
|
||||
|
||||
|
@ -347,7 +347,7 @@ namespace BTCPayServer.Controllers
|
||||
if (appIdsToFetch.Any())
|
||||
{
|
||||
var apps = (await _AppService.GetApps(appIdsToFetch.ToArray()))
|
||||
.ToDictionary(data => data.Id, data => Enum.Parse<AppType>(data.AppType));
|
||||
.ToDictionary(data => data.Id, data => data.AppType);
|
||||
;
|
||||
if (!string.IsNullOrEmpty(settings.RootAppId))
|
||||
{
|
||||
@ -422,8 +422,10 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
private async Task<List<SelectListItem>> GetAppSelectList()
|
||||
{
|
||||
var types = _AppService.GetAvailableAppTypes();
|
||||
var apps = (await _AppService.GetAllApps(null, true))
|
||||
.Select(a => new SelectListItem($"{typeof(AppType).DisplayName(a.AppType)} - {a.AppName} - {a.StoreName}", a.Id)).ToList();
|
||||
.Select(a =>
|
||||
new SelectListItem($"{types[a.AppType]} - {a.AppName} - {a.StoreName}", a.Id)).ToList();
|
||||
apps.Insert(0, new SelectListItem("(None)", null));
|
||||
return apps;
|
||||
}
|
||||
|
@ -34,6 +34,7 @@ namespace BTCPayServer.Controllers
|
||||
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
|
||||
private readonly IEnumerable<IPayoutHandler> _payoutHandlers;
|
||||
private readonly CurrencyNameTable _currencyNameTable;
|
||||
private readonly DisplayFormatter _displayFormatter;
|
||||
private readonly PullPaymentHostedService _pullPaymentService;
|
||||
private readonly ApplicationDbContextFactory _dbContextFactory;
|
||||
private readonly BTCPayNetworkJsonSerializerSettings _jsonSerializerSettings;
|
||||
@ -49,6 +50,7 @@ namespace BTCPayServer.Controllers
|
||||
public UIStorePullPaymentsController(BTCPayNetworkProvider btcPayNetworkProvider,
|
||||
IEnumerable<IPayoutHandler> payoutHandlers,
|
||||
CurrencyNameTable currencyNameTable,
|
||||
DisplayFormatter displayFormatter,
|
||||
PullPaymentHostedService pullPaymentHostedService,
|
||||
ApplicationDbContextFactory dbContextFactory,
|
||||
BTCPayNetworkJsonSerializerSettings jsonSerializerSettings)
|
||||
@ -56,6 +58,7 @@ namespace BTCPayServer.Controllers
|
||||
_btcPayNetworkProvider = btcPayNetworkProvider;
|
||||
_payoutHandlers = payoutHandlers;
|
||||
_currencyNameTable = currencyNameTable;
|
||||
_displayFormatter = displayFormatter;
|
||||
_pullPaymentService = pullPaymentHostedService;
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_jsonSerializerSettings = jsonSerializerSettings;
|
||||
@ -532,7 +535,7 @@ namespace BTCPayServer.Controllers
|
||||
PullPaymentName = ppBlob?.Name ?? item.PullPayment?.Id,
|
||||
Date = item.Payout.Date,
|
||||
PayoutId = item.Payout.Id,
|
||||
Amount = _currencyNameTable.DisplayFormatCurrency(payoutBlob.Amount, ppBlob?.Currency ?? PaymentMethodId.Parse(item.Payout.PaymentMethodId).CryptoCode),
|
||||
Amount = _displayFormatter.Currency(payoutBlob.Amount, ppBlob?.Currency ?? PaymentMethodId.Parse(item.Payout.PaymentMethodId).CryptoCode),
|
||||
Destination = payoutBlob.Destination
|
||||
};
|
||||
var handler = _payoutHandlers
|
||||
|
@ -42,6 +42,8 @@ namespace BTCPayServer.Controllers
|
||||
return View(vm);
|
||||
|
||||
var userId = GetUserId();
|
||||
if (userId is null)
|
||||
return NotFound();
|
||||
var apps = await _appService.GetAllApps(userId, false, store.Id);
|
||||
foreach (var app in apps)
|
||||
{
|
||||
|
@ -16,12 +16,6 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class UIStoresController
|
||||
{
|
||||
[HttpGet("{storeId}/plugins")]
|
||||
public IActionResult Plugins()
|
||||
{
|
||||
return View("Plugins", new PluginsViewModel());
|
||||
}
|
||||
|
||||
private async Task<Data.WebhookDeliveryData?> LastDeliveryForWebhook(string webhookId)
|
||||
{
|
||||
return (await _Repo.GetWebhookDeliveries(CurrentStore.Id, webhookId, 1)).ToList().FirstOrDefault();
|
||||
|
@ -27,7 +27,6 @@ using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
@ -387,7 +386,9 @@ namespace BTCPayServer.Controllers
|
||||
}).ToList();
|
||||
|
||||
vm.UseNewCheckout = storeBlob.CheckoutType == Client.Models.CheckoutType.V2;
|
||||
vm.CelebratePayment = storeBlob.CelebratePayment;
|
||||
vm.OnChainWithLnInvoiceFallback = storeBlob.OnChainWithLnInvoiceFallback;
|
||||
vm.LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi;
|
||||
vm.RequiresRefundEmail = storeBlob.RequiresRefundEmail;
|
||||
vm.LazyPaymentMethods = storeBlob.LazyPaymentMethods;
|
||||
vm.RedirectAutomatically = storeBlob.RedirectAutomatically;
|
||||
@ -505,8 +506,9 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
blob.CheckoutType = model.UseNewCheckout ? Client.Models.CheckoutType.V2 : Client.Models.CheckoutType.V1;
|
||||
|
||||
blob.CelebratePayment = model.CelebratePayment;
|
||||
blob.OnChainWithLnInvoiceFallback = model.OnChainWithLnInvoiceFallback;
|
||||
blob.LightningAmountInSatoshi = model.LightningAmountInSatoshi;
|
||||
blob.RequiresRefundEmail = model.RequiresRefundEmail;
|
||||
blob.LazyPaymentMethods = model.LazyPaymentMethods;
|
||||
blob.RedirectAutomatically = model.RedirectAutomatically;
|
||||
|
@ -7,6 +7,7 @@ using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.BIP78.Sender;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.ModelBinders;
|
||||
using BTCPayServer.Models;
|
||||
@ -266,7 +267,7 @@ namespace BTCPayServer.Controllers
|
||||
ModelState.Remove(nameof(vm.PSBT));
|
||||
ModelState.Remove(nameof(vm.FileName));
|
||||
ModelState.Remove(nameof(vm.UploadedPSBTFile));
|
||||
await FetchTransactionDetails(derivationSchemeSettings, vm, network);
|
||||
await FetchTransactionDetails(walletId,derivationSchemeSettings, vm, network);
|
||||
return View("WalletPSBTDecoded", vm);
|
||||
|
||||
case "save-psbt":
|
||||
@ -320,7 +321,7 @@ namespace BTCPayServer.Controllers
|
||||
return await _payjoinClient.RequestPayjoin(bip21, new PayjoinWallet(derivationSchemeSettings), psbt, cts.Token);
|
||||
}
|
||||
|
||||
private async Task FetchTransactionDetails(DerivationSchemeSettings derivationSchemeSettings, WalletPSBTReadyViewModel vm, BTCPayNetwork network)
|
||||
private async Task FetchTransactionDetails(WalletId walletId, DerivationSchemeSettings derivationSchemeSettings, WalletPSBTReadyViewModel vm, BTCPayNetwork network)
|
||||
{
|
||||
var psbtObject = PSBT.Parse(vm.SigningContext.PSBT, network.NBitcoinNetwork);
|
||||
if (!psbtObject.IsAllFinalized())
|
||||
@ -371,17 +372,29 @@ namespace BTCPayServer.Controllers
|
||||
vm.Positive = balanceChange >= Money.Zero;
|
||||
}
|
||||
vm.Inputs = new List<WalletPSBTReadyViewModel.InputViewModel>();
|
||||
var inputToObjects = new Dictionary<uint, ObjectTypeId[]>();
|
||||
var outputToObjects = new Dictionary<string, ObjectTypeId>();
|
||||
foreach (var input in psbtObject.Inputs)
|
||||
{
|
||||
var inputVm = new WalletPSBTReadyViewModel.InputViewModel();
|
||||
vm.Inputs.Add(inputVm);
|
||||
var txOut = input.GetTxOut();
|
||||
var mine = input.HDKeysFor(derivationSchemeSettings.AccountDerivation, signingKey, signingKeyPath).Any();
|
||||
var balanceChange2 = input.GetTxOut()?.Value ?? Money.Zero;
|
||||
var balanceChange2 = txOut?.Value ?? Money.Zero;
|
||||
if (mine)
|
||||
balanceChange2 = -balanceChange2;
|
||||
inputVm.BalanceChange = ValueToString(balanceChange2, network);
|
||||
inputVm.Positive = balanceChange2 >= Money.Zero;
|
||||
inputVm.Index = (int)input.Index;
|
||||
|
||||
var walletObjectIds = new List<ObjectTypeId>();
|
||||
walletObjectIds.Add(new ObjectTypeId(WalletObjectData.Types.Utxo, input.PrevOut.ToString()));
|
||||
walletObjectIds.Add(new ObjectTypeId(WalletObjectData.Types.Tx, input.PrevOut.Hash.ToString()));
|
||||
var address = txOut?.ScriptPubKey.GetDestinationAddress(network.NBitcoinNetwork)?.ToString();
|
||||
if(address != null)
|
||||
walletObjectIds.Add(new ObjectTypeId(WalletObjectData.Types.Address, address));
|
||||
inputToObjects.Add(input.Index, walletObjectIds.ToArray());
|
||||
|
||||
}
|
||||
vm.Destinations = new List<WalletPSBTReadyViewModel.DestinationViewModel>();
|
||||
foreach (var output in psbtObject.Outputs)
|
||||
@ -395,6 +408,10 @@ namespace BTCPayServer.Controllers
|
||||
dest.Balance = ValueToString(balanceChange2, network);
|
||||
dest.Positive = balanceChange2 >= Money.Zero;
|
||||
dest.Destination = output.ScriptPubKey.GetDestinationAddress(network.NBitcoinNetwork)?.ToString() ?? output.ScriptPubKey.ToString();
|
||||
var address = output.ScriptPubKey.GetDestinationAddress(network.NBitcoinNetwork)?.ToString();
|
||||
if(address != null)
|
||||
outputToObjects.Add(dest.Destination, new ObjectTypeId(WalletObjectData.Types.Address, address));
|
||||
|
||||
}
|
||||
|
||||
if (psbtObject.TryGetFee(out var fee))
|
||||
@ -420,6 +437,38 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
vm.SetErrors(errors);
|
||||
}
|
||||
|
||||
var combinedTypeIds = inputToObjects.Values.SelectMany(ids => ids).Concat(outputToObjects.Values)
|
||||
.DistinctBy(id => $"{id.Type}:{id.Id}").ToArray();
|
||||
|
||||
var labelInfo = await WalletRepository.GetWalletTransactionsInfo(walletId, combinedTypeIds);
|
||||
foreach (KeyValuePair<uint,ObjectTypeId[]> inputToObject in inputToObjects)
|
||||
{
|
||||
var keys = inputToObject.Value.Select(id => id.Id).ToArray();
|
||||
WalletTransactionInfo ix = null;
|
||||
foreach (var key in keys)
|
||||
{
|
||||
if (!labelInfo.TryGetValue(key, out var i)) continue;
|
||||
if (ix is null)
|
||||
{
|
||||
ix = i;
|
||||
}
|
||||
else
|
||||
{
|
||||
ix.Merge(i);
|
||||
}
|
||||
}
|
||||
if (ix is null) continue;
|
||||
var input = vm.Inputs.First(model => model.Index == inputToObject.Key);
|
||||
input.Labels = ix.LabelColors;
|
||||
}
|
||||
foreach (var outputToObject in outputToObjects)
|
||||
{
|
||||
if (!labelInfo.TryGetValue(outputToObject.Value.Id, out var ix)) continue;
|
||||
var destination = vm.Destinations.First(model => model.Destination == outputToObject.Key);
|
||||
destination.Labels = ix.LabelColors;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
[HttpPost("{walletId}/psbt/ready")]
|
||||
@ -439,7 +488,7 @@ namespace BTCPayServer.Controllers
|
||||
if (derivationSchemeSettings == null)
|
||||
return NotFound();
|
||||
|
||||
await FetchTransactionDetails(derivationSchemeSettings, vm, network);
|
||||
await FetchTransactionDetails(walletId,derivationSchemeSettings, vm, network);
|
||||
|
||||
switch (command)
|
||||
{
|
||||
@ -570,7 +619,7 @@ namespace BTCPayServer.Controllers
|
||||
BackUrl = vm.BackUrl
|
||||
});
|
||||
case "decode":
|
||||
await FetchTransactionDetails(derivationSchemeSettings, vm, network);
|
||||
await FetchTransactionDetails(walletId,derivationSchemeSettings, vm, network);
|
||||
return View("WalletPSBTDecoded", vm);
|
||||
default:
|
||||
vm.Errors.Add("Unknown command");
|
||||
|
@ -235,8 +235,7 @@ namespace BTCPayServer.Controllers
|
||||
var model = new ListTransactionsViewModel { Skip = skip, Count = count };
|
||||
model.Labels.AddRange(
|
||||
(await WalletRepository.GetWalletLabels(walletId))
|
||||
.Select(c => (c.Label, c.Color, ColorPalette.Default.TextColor(c.Color)))
|
||||
);
|
||||
.Select(c => (c.Label, c.Color, ColorPalette.Default.TextColor(c.Color))));
|
||||
|
||||
if (labelFilter != null)
|
||||
{
|
||||
@ -733,6 +732,18 @@ namespace BTCPayServer.Controllers
|
||||
if (!ModelState.IsValid)
|
||||
return View(vm);
|
||||
|
||||
foreach (var transactionOutput in vm.Outputs.Where(output => output.Labels?.Any() is true))
|
||||
{
|
||||
var labels = transactionOutput.Labels.Where(s => !string.IsNullOrWhiteSpace(s)).ToArray();
|
||||
var walletObjectAddress = new WalletObjectId(walletId, WalletObjectData.Types.Address, transactionOutput.DestinationAddress.ToLowerInvariant());
|
||||
var obj = await WalletRepository.GetWalletObject(walletObjectAddress);
|
||||
if (obj is null)
|
||||
{
|
||||
await WalletRepository.EnsureWalletObject(walletObjectAddress);
|
||||
}
|
||||
await WalletRepository.AddWalletObjectLabels(walletObjectAddress, labels);
|
||||
}
|
||||
|
||||
var derivationScheme = GetDerivationSchemeSettings(walletId);
|
||||
if (derivationScheme is null)
|
||||
return NotFound();
|
||||
@ -1306,36 +1317,51 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
var wallet = _walletProvider.GetWallet(paymentMethod.Network);
|
||||
var walletTransactionsInfoAsync = WalletRepository.GetWalletTransactionsInfo(walletId, (string[]?)null);
|
||||
var input = await wallet.FetchTransactionHistory(paymentMethod.AccountDerivation, null, null);
|
||||
var input = await wallet.FetchTransactionHistory(paymentMethod.AccountDerivation);
|
||||
var walletTransactionsInfo = await walletTransactionsInfoAsync;
|
||||
var export = new TransactionsExport(wallet, walletTransactionsInfo);
|
||||
var res = export.Process(input, format);
|
||||
|
||||
var fileType = format switch
|
||||
{
|
||||
"csv" => "csv",
|
||||
"json" => "json",
|
||||
"bip329" => "jsonl",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(format), format, null)
|
||||
};
|
||||
var mimeType = format switch
|
||||
{
|
||||
"csv" => "text/csv",
|
||||
"json" => "application/json",
|
||||
"bip329" => "text/jsonl", // https://stackoverflow.com/questions/59938644/what-is-the-mime-type-of-jsonl-files
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(format), format, null)
|
||||
};
|
||||
var cd = new ContentDisposition
|
||||
{
|
||||
FileName = $"btcpay-{walletId}-{DateTime.UtcNow.ToString("yyyyMMdd-HHmmss", CultureInfo.InvariantCulture)}.{format}",
|
||||
FileName = $"btcpay-{walletId}-{DateTime.UtcNow.ToString("yyyyMMdd-HHmmss", CultureInfo.InvariantCulture)}.{fileType}",
|
||||
Inline = true
|
||||
};
|
||||
Response.Headers.Add("Content-Disposition", cd.ToString());
|
||||
Response.Headers.Add("X-Content-Type-Options", "nosniff");
|
||||
return Content(res, "application/" + format);
|
||||
return Content(res, mimeType);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public class UpdateLabelsRequest
|
||||
{
|
||||
public string? Address { get; set; }
|
||||
public string? Id { get; set; }
|
||||
public string? Type { get; set; }
|
||||
public string[]? Labels { get; set; }
|
||||
}
|
||||
|
||||
[HttpPost("{walletId}/update-labels")]
|
||||
[IgnoreAntiforgeryToken]
|
||||
public async Task<IActionResult> UpdateLabels([ModelBinder(typeof(WalletIdModelBinder))] WalletId walletId, [FromBody] UpdateLabelsRequest request)
|
||||
public async Task<IActionResult> UpdateLabels(
|
||||
[ModelBinder(typeof(WalletIdModelBinder))] WalletId walletId,
|
||||
[FromBody] UpdateLabelsRequest request)
|
||||
{
|
||||
if (string.IsNullOrEmpty(request.Address) || request.Labels is null)
|
||||
if (string.IsNullOrEmpty(request.Type) || string.IsNullOrEmpty(request.Id) || request.Labels is null)
|
||||
return BadRequest();
|
||||
|
||||
var objid = new WalletObjectId(walletId, WalletObjectData.Types.Address, request.Address);
|
||||
var objid = new WalletObjectId(walletId, request.Type, request.Id);
|
||||
var obj = await WalletRepository.GetWalletObject(objid);
|
||||
if (obj is null)
|
||||
{
|
||||
@ -1353,17 +1379,26 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpGet("{walletId}/labels")]
|
||||
[IgnoreAntiforgeryToken]
|
||||
public async Task<IActionResult> GetLabels( [ModelBinder(typeof(WalletIdModelBinder))] WalletId walletId, bool excludeTypes)
|
||||
public async Task<IActionResult> GetLabels(
|
||||
[ModelBinder(typeof(WalletIdModelBinder))] WalletId walletId,
|
||||
bool excludeTypes,
|
||||
string? type = null,
|
||||
string? id = null)
|
||||
{
|
||||
|
||||
return Ok(( await WalletRepository.GetWalletLabels(walletId))
|
||||
.Where(l => !excludeTypes || !WalletObjectData.Types.AllTypes.Contains(l.Label))
|
||||
.Select(tuple => new
|
||||
{
|
||||
label = tuple.Label,
|
||||
color = tuple.Color,
|
||||
textColor = ColorPalette.Default.TextColor(tuple.Color)
|
||||
}));
|
||||
var walletObjectId = !string.IsNullOrEmpty(type) && !string.IsNullOrEmpty(id)
|
||||
? new WalletObjectId(walletId, type, id)
|
||||
: null;
|
||||
var labels = walletObjectId == null
|
||||
? await WalletRepository.GetWalletLabels(walletId)
|
||||
: await WalletRepository.GetWalletLabels(walletObjectId);
|
||||
return Ok(labels
|
||||
.Where(l => !excludeTypes || !WalletObjectData.Types.AllTypes.Contains(l.Label))
|
||||
.Select(tuple => new
|
||||
{
|
||||
label = tuple.Label,
|
||||
color = tuple.Color,
|
||||
textColor = ColorPalette.Default.TextColor(tuple.Color)
|
||||
}));
|
||||
}
|
||||
|
||||
private string GetImage(PaymentMethodId paymentMethodId, BTCPayNetwork network)
|
||||
@ -1433,8 +1468,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
0 => PayoutTooltip(),
|
||||
1 => PayoutTooltip(payoutsByPullPaymentId.First()),
|
||||
_ =>
|
||||
$"<ul>{string.Join(string.Empty, payoutsByPullPaymentId.Select(pair => $"<li>{PayoutTooltip(pair)}</li>"))}</ul>"
|
||||
_ => string.Join(", ", payoutsByPullPaymentId.Select(PayoutTooltip))
|
||||
};
|
||||
|
||||
model.Link = _linkGenerator.PayoutLink(transactionInfo.WalletId.ToString(), null, PayoutState.Completed, Request.Scheme, Request.Host,
|
||||
@ -1442,7 +1476,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
else if (tag.Type == WalletObjectData.Types.Payjoin)
|
||||
{
|
||||
model.Tooltip = $"This UTXO was part of a PayJoin transaction.";
|
||||
model.Tooltip = "This UTXO was part of a PayJoin transaction.";
|
||||
}
|
||||
else if (tag.Type == WalletObjectData.Types.Invoice)
|
||||
{
|
||||
|
@ -218,6 +218,10 @@ namespace BTCPayServer.Data
|
||||
public string BrandColor { get; set; }
|
||||
public string LogoFileId { get; set; }
|
||||
public string CssFileId { get; set; }
|
||||
|
||||
[DefaultValue(true)]
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
|
||||
public bool CelebratePayment { get; set; } = true;
|
||||
|
||||
public IPaymentFilter GetExcludedPaymentMethods()
|
||||
{
|
||||
|
@ -1,8 +1,10 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Services.Rates;
|
||||
@ -14,6 +16,33 @@ namespace BTCPayServer.Data
|
||||
{
|
||||
public static class StoreDataExtensions
|
||||
{
|
||||
public static PermissionSet GetPermissionSet(this StoreData store)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(store);
|
||||
if (store.Role is null)
|
||||
return new PermissionSet();
|
||||
return new PermissionSet(store.Role == StoreRoles.Owner
|
||||
? new[]
|
||||
{
|
||||
Permission.Create(Policies.CanModifyStoreSettings, store.Id),
|
||||
Permission.Create(Policies.CanTradeCustodianAccount, store.Id),
|
||||
Permission.Create(Policies.CanWithdrawFromCustodianAccounts, store.Id),
|
||||
Permission.Create(Policies.CanDepositToCustodianAccounts, store.Id)
|
||||
}
|
||||
: new[]
|
||||
{
|
||||
Permission.Create(Policies.CanViewStoreSettings, store.Id),
|
||||
Permission.Create(Policies.CanModifyInvoices, store.Id),
|
||||
Permission.Create(Policies.CanViewCustodianAccounts, store.Id),
|
||||
Permission.Create(Policies.CanDepositToCustodianAccounts, store.Id)
|
||||
});
|
||||
}
|
||||
public static bool HasPermission(this StoreData store, string permission)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(store);
|
||||
return store.GetPermissionSet().Contains(permission, store.Id);
|
||||
}
|
||||
|
||||
#pragma warning disable CS0618
|
||||
public static PaymentMethodId? GetDefaultPaymentId(this StoreData storeData)
|
||||
{
|
||||
|
@ -49,6 +49,7 @@ namespace BTCPayServer
|
||||
{
|
||||
return AccountDerivation is null ? null : DBUtils.nbxv1_get_wallet_id(Network.CryptoCode, AccountDerivation.ToString());
|
||||
}
|
||||
|
||||
private static bool TryParseXpub(string xpub, DerivationSchemeParser derivationSchemeParser, ref DerivationSchemeSettings derivationSchemeSettings, ref string error, bool electrum = true)
|
||||
{
|
||||
if (!electrum)
|
||||
@ -78,15 +79,36 @@ namespace BTCPayServer
|
||||
}
|
||||
try
|
||||
{
|
||||
// Extract fingerprint and account key path from export formats that contain them.
|
||||
// Possible formats: [fingerprint/account_key_path]xpub, [fingerprint]xpub, xpub
|
||||
HDFingerprint? rootFingerprint = null;
|
||||
KeyPath accountKeyPath = null;
|
||||
var derivationRegex = new Regex(@"^(?:\[(\w+)(?:\/(.*?))?\])?(\w+)$", RegexOptions.IgnoreCase);
|
||||
var match = derivationRegex.Match(xpub.Trim());
|
||||
if (match.Success)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(match.Groups[1].Value)) rootFingerprint = HDFingerprint.Parse(match.Groups[1].Value);
|
||||
if (!string.IsNullOrEmpty(match.Groups[2].Value)) accountKeyPath = KeyPath.Parse(match.Groups[2].Value);
|
||||
if (!string.IsNullOrEmpty(match.Groups[3].Value)) xpub = match.Groups[3].Value;
|
||||
}
|
||||
derivationSchemeSettings.AccountOriginal = xpub.Trim();
|
||||
derivationSchemeSettings.AccountDerivation = electrum ? derivationSchemeParser.ParseElectrum(derivationSchemeSettings.AccountOriginal) : derivationSchemeParser.Parse(derivationSchemeSettings.AccountOriginal);
|
||||
derivationSchemeSettings.AccountKeySettings = derivationSchemeSettings.AccountDerivation.GetExtPubKeys()
|
||||
.Select(key => new AccountKeySettings()
|
||||
.Select(key => new AccountKeySettings
|
||||
{
|
||||
AccountKey = key.GetWif(derivationSchemeParser.Network)
|
||||
}).ToArray();
|
||||
if (derivationSchemeSettings.AccountDerivation is DirectDerivationStrategy direct && !direct.Segwit)
|
||||
derivationSchemeSettings.AccountOriginal = null; // Saving this would be confusing for user, as xpub of electrum is legacy derivation, but for btcpay, it is segwit derivation
|
||||
// apply initial matches if there were no results from parsing
|
||||
if (rootFingerprint != null && derivationSchemeSettings.AccountKeySettings[0].RootFingerprint == null)
|
||||
{
|
||||
derivationSchemeSettings.AccountKeySettings[0].RootFingerprint = rootFingerprint;
|
||||
}
|
||||
if (accountKeyPath != null && derivationSchemeSettings.AccountKeySettings[0].AccountKeyPath == null)
|
||||
{
|
||||
derivationSchemeSettings.AccountKeySettings[0].AccountKeyPath = accountKeyPath;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
catch (Exception exception)
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user