Compare commits
29 Commits
Author | SHA1 | Date | |
---|---|---|---|
78f33f0ca4 | |||
a7e3cbb105 | |||
8b5c5895f0 | |||
9d5baabc2c | |||
a7e910f7ff | |||
148e31721b | |||
df16ad6418 | |||
1c8ded9362 | |||
9e58a50dfd | |||
63750d69c4 | |||
cf6a356e08 | |||
1c6657b8b4 | |||
32f1d5ea1d | |||
1d44cad847 | |||
88d8d1b848 | |||
df82860ada | |||
b184067df7 | |||
31c1d4795f | |||
e59684fc6a | |||
ddea59cb1b | |||
c37584328b | |||
60b317a972 | |||
4fbcd89bb6 | |||
039e613524 | |||
192d339a79 | |||
7faf95552a | |||
bc1a1cf34f | |||
9f4acdf8be | |||
8fc4aefa8f |
20
BTCPayServer.Client/BTCPayServerClient.ServerEmail.cs
Normal file
20
BTCPayServer.Client/BTCPayServerClient.ServerEmail.cs
Normal file
@ -0,0 +1,20 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
|
||||
namespace BTCPayServer.Client;
|
||||
|
||||
public partial class BTCPayServerClient
|
||||
{
|
||||
public virtual async Task<ServerEmailSettingsData> GetServerEmailSettings(CancellationToken token = default)
|
||||
{
|
||||
return await SendHttpRequest<ServerEmailSettingsData>("api/v1/server/email", null, HttpMethod.Get, token);
|
||||
}
|
||||
|
||||
public virtual async Task<bool> UpdateServerEmailSettings(ServerEmailSettingsData request, CancellationToken token = default)
|
||||
{
|
||||
return await SendHttpRequest<bool>("api/v1/server/email", request, HttpMethod.Put, token);
|
||||
}
|
||||
}
|
@ -4,29 +4,11 @@ namespace BTCPayServer.Client.Models;
|
||||
|
||||
public class EmailSettingsData
|
||||
{
|
||||
public string Server
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public int? Port
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string Login
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string Password
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string From
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string Server { get; set; }
|
||||
public int? Port { get; set; }
|
||||
public string Login { get; set; }
|
||||
public string Password { get; set; }
|
||||
public string From { get; set; }
|
||||
public bool DisableCertificateCheck { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
|
7
BTCPayServer.Client/Models/ServerEmailSettingsData.cs
Normal file
7
BTCPayServer.Client/Models/ServerEmailSettingsData.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public class ServerEmailSettingsData : EmailSettingsData
|
||||
{
|
||||
public bool EnableStoresToUseServerEmailSettings { get; set; }
|
||||
}
|
||||
}
|
@ -2,7 +2,7 @@
|
||||
<Import Project="../Build/Version.csproj" Condition="Exists('../Build/Version.csproj')" />
|
||||
<Import Project="../Build/Common.csproj" />
|
||||
<ItemGroup>
|
||||
<PackageReference Include="NBXplorer.Client" Version="4.3.6" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="4.3.9" />
|
||||
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="2.0.1" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
@ -38,8 +38,6 @@ namespace BTCPayServer.Rating
|
||||
ArgumentNullException.ThrowIfNull(str);
|
||||
value = null;
|
||||
str = str.Trim();
|
||||
if (str.Length > 12)
|
||||
return false;
|
||||
var splitted = str.Split(new[] { '_', '-' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (splitted.Length == 2)
|
||||
{
|
||||
|
@ -44,9 +44,9 @@
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="8.0.11" />
|
||||
<PackageReference Include="Selenium.Support" Version="4.1.1" />
|
||||
<PackageReference Include="Selenium.WebDriver" Version="4.22.0" />
|
||||
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="128.0.6613.11900" />
|
||||
<PackageReference Include="xunit" Version="2.9.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
|
||||
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="133.0.6943.5300" />
|
||||
<PackageReference Include="xunit" Version="2.9.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
@ -19,6 +19,19 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
public static class Extensions
|
||||
{
|
||||
public static Task<NewTransactionEvent> WaitReceive(this NBXplorer.WebsocketNotificationSession notifications, DerivationStrategyBase target, Func<NewTransactionEvent, bool> predicate = null, CancellationToken cancellationToken = default)
|
||||
=> WaitNext<NewTransactionEvent>(notifications, e => e.DerivationStrategy == target && (predicate is null || predicate(e)), cancellationToken);
|
||||
public static async Task<TEvent> WaitNext<TEvent>(this NBXplorer.WebsocketNotificationSession notifications, Func<TEvent, bool> predicate, CancellationToken cancellationToken = default) where TEvent : NewEventBase
|
||||
{
|
||||
retry:
|
||||
var evt = await notifications.NextEventAsync(cancellationToken);
|
||||
if (evt is TEvent { } e)
|
||||
{
|
||||
if (predicate(e))
|
||||
return e;
|
||||
}
|
||||
goto retry;
|
||||
}
|
||||
public static Task<KeyPathInformation> ReserveAddressAsync(this BTCPayWallet wallet, DerivationStrategyBase derivationStrategyBase)
|
||||
{
|
||||
return wallet.ReserveAddressAsync(null, derivationStrategyBase, "test");
|
||||
|
227
BTCPayServer.Tests/FeatureTests/MultisigTests.cs
Normal file
227
BTCPayServer.Tests/FeatureTests/MultisigTests.cs
Normal file
@ -0,0 +1,227 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.BIP78.Sender;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Views.Wallets;
|
||||
using NBitcoin;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using NBXplorer.Models;
|
||||
using OpenQA.Selenium;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace BTCPayServer.Tests.FeatureTests;
|
||||
|
||||
[Collection(nameof(NonParallelizableCollectionDefinition))]
|
||||
public class MultisigTests : UnitTestBase
|
||||
{
|
||||
public MultisigTests(ITestOutputHelper helper) : base(helper)
|
||||
{
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Selenium", "Selenium")]
|
||||
public async Task SignTestPSBT()
|
||||
{
|
||||
var cryptoCode = "BTC";
|
||||
using var s = CreateSeleniumTester();
|
||||
await s.StartAsync();
|
||||
|
||||
var network = s.Server.NetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
|
||||
var resp1 = generateWalletResp("tprv8ZgxMBicQKsPeGSkDtxjScBmmHP4rfSEPkf1vNmoqt5QjPTco2zPd6UVWkJf2fU8gdKPYRdDMizxtMRqmpVpxsWuqRxVs2d5VsEhwxaK3h7",
|
||||
"57b3f43a/84'/1'/0'", "tpubDCzBHRPRcv7Y3utw1hZVrCar21gsj8vsXcehAG4z3R4NnmdMAASQwYYxGBd2f4q5s5ZFGvQBBFs1jVcGsXYoSTA1YFQPwizjsQLU12ibLyu", network);
|
||||
var resp2 = generateWalletResp("tprv8ZgxMBicQKsPeC6Xuw83UJHgjnszEUjwH9E5f5FZ3fHgJHBQApo8CmFCsowcdwbRM119UnTqSzVWUsWGtLsxc8wnZa5L8xmEsvEpiyRj4Js",
|
||||
"ee7d36c4/84'/1'/0'", "tpubDCetxnEjn8HXA5NrDZbKKTUUYoWCVC2V3X7Kmh3o9UYTfh9c3wTPKyCyeUrLkQ8KHYptEsBoQq6AgqPZiW5neEgb2kjKEr41q1qSevoPFDM", network);
|
||||
var resp3 = generateWalletResp("tprv8ZgxMBicQKsPekSniuKwLtXpB82dSDV8ZAK4uLUHxkiHWfDtR5yYwNZiicKdpT3UYwzTTMvXESCm45KyAiH7kiJY6yk51neC9ZvmwDpNsQh",
|
||||
"6c014fb3/84'/1'/0'", "tpubDCaTgjJfS5UEim6h66VpQBEZ2Tj6hHk8TzvL81HygdW1M8vZCRhUZLNhb3WTimyP2XMQRA3QGZPwwxUsEFQYK4EoRUWTcb9oB237FJ112tN", network);
|
||||
|
||||
var multisigDerivationScheme = $"wsh(multi(2,[{resp1.AccountKeyPath}]{resp1.DerivationScheme}/0/*," +
|
||||
$"[{resp2.AccountKeyPath}]{resp2.DerivationScheme}/0/*," +
|
||||
$"[{resp3.AccountKeyPath}]{resp3.DerivationScheme}/0/*))";
|
||||
|
||||
var strategy = UIStoresController.ParseDerivationStrategy(multisigDerivationScheme, network);
|
||||
strategy.Source = "ManualDerivationScheme";
|
||||
var derivationScheme = strategy.AccountDerivation;
|
||||
|
||||
var testPSBT =
|
||||
"cHNidP8BAIkCAAAAAQmiSunnaKN7F4Jv5uHROfYbIZOckCck/Wo7gAQmi9hfAAAAAAD9////AtgbZgAAAAAAIgAgWCUFlU9eWkyxn0l0yQxs2rXQZ7d9Ry8LaYECaVC0TUGAlpgAAAAAACIAIFZxT+UIdhHZC4qFPhPQ6IXdX+44HIxCYcoh/bNOhB0hAAAAAAABAStAAf8AAAAAACIAIL2DDkfKwKHxZj2EKxXUd4uwf0IvPaCxUtAPq9snpq9TAQDqAgAAAAABAVuHuou9E5y6zUJaUreQD0wUeiPnT2aY+YU7QaPJOiQCAAAAAAD9////AkAB/wAAAAAAIgAgvYMOR8rAofFmPYQrFdR3i7B/Qi89oLFS0A+r2yemr1PM5AYpAQAAABYAFIlFupZkD07+GRo24WRS3IFcf+EuAkcwRAIgGi9wAcTfc0d0+j+Vg82aYklXCUsPg+g3jS+PTBTSQwkCIAPh5CZF18DTBKqWU2qdhNCbZ8Tp/NCEHjLJRHcH0oluASECWnI1s9ozQRL2qbK6JbLHzj9LlU9Pras3nZfq/njBJwhwAAAAAQVpUiECMCCasr2FRmRMiWkM/l1iraFR18td5SZ2APyQiaI0yY8hA8K96vH64BelUJiEPGwM6UTwRSfAJUR2j8dkw7i31fFTIQMlHLlaAPxw3fl1vaM1EofIirt79MXOryM54zpHwu1GlVOuIgIDwr3q8frgF6VQmIQ8bAzpRPBFJ8AlRHaPx2TDuLfV8VNHMEQCIANnprskJz8oVsetqOEViHtzhmSG8c36r3zmUIHwIoOhAiAZ1jBqj40iu2S/nMfiGyuCC/jSiSGik7YVwiwN+bbxPAEiBgIwIJqyvYVGZEyJaQz+XWKtoVHXy13lJnYA/JCJojTJjxhXs/Q6VAAAgAEAAIAAAACAAAAAAAUAAAAiBgMlHLlaAPxw3fl1vaM1EofIirt79MXOryM54zpHwu1GlRhsAU+zVAAAgAEAAIAAAACAAAAAAAUAAAAiBgPCverx+uAXpVCYhDxsDOlE8EUnwCVEdo/HZMO4t9XxUxjufTbEVAAAgAEAAIAAAACAAAAAAAUAAAAAAQFpUiEDa/J6SaiRjP1jhq9jpNxFKovEuWBz28seNMvsn0JC/ZIhA7p3bS7vLYB5UxlNN6YqkEDITyaMlk/i450q6+4woveAIQPTchIOrd+TNGBOX6il1HRZnBndyRoUj/hahbjTaAGHglOuIgIDa/J6SaiRjP1jhq9jpNxFKovEuWBz28seNMvsn0JC/ZIYV7P0OlQAAIABAACAAAAAgAEAAAABAAAAIgIDundtLu8tgHlTGU03piqQQMhPJoyWT+LjnSrr7jCi94AY7n02xFQAAIABAACAAAAAgAEAAAABAAAAIgID03ISDq3fkzRgTl+opdR0WZwZ3ckaFI/4WoW402gBh4IYbAFPs1QAAIABAACAAAAAgAEAAAABAAAAAAEBaVIhA/fCRR3MWwCgNuXMvlWLonY+TurUKOHXOSHALCck62deIQPqeQXD8ws9SDEDXSyD6a3WFlIGH+gDUf2/xAfw8HxE8iEC3LBRJYYxRzIeg9NxLGvtfATvFaKsO9D7AUjoTLZzke5TriICAtywUSWGMUcyHoPTcSxr7XwE7xWirDvQ+wFI6Ey2c5HuGGwBT7NUAACAAQAAgAAAAIAAAAAADAAAACICA+p5BcPzCz1IMQNdLIPprdYWUgYf6ANR/b/EB/DwfETyGO59NsRUAACAAQAAgAAAAIAAAAAADAAAACICA/fCRR3MWwCgNuXMvlWLonY+TurUKOHXOSHALCck62deGFez9DpUAACAAQAAgAAAAIAAAAAADAAAAAA=";
|
||||
|
||||
var signedPsbt = SignWithSeed(testPSBT, derivationScheme, resp1);
|
||||
s.TestLogs.LogInformation($"Signed PSBT: {signedPsbt}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Selenium", "Selenium")]
|
||||
public async Task CanEnableAndUseMultisigWallet()
|
||||
{
|
||||
var cryptoCode = "BTC";
|
||||
using var s = CreateSeleniumTester();
|
||||
await s.StartAsync();
|
||||
// var invoiceRepository = s.Server.PayTester.GetService<InvoiceRepository>();
|
||||
s.RegisterNewUser(true);
|
||||
|
||||
var storeData = s.CreateNewStore();
|
||||
|
||||
var explorerProvider = s.Server.PayTester.GetService<ExplorerClientProvider>();
|
||||
var client = explorerProvider.GetExplorerClient(cryptoCode);
|
||||
var req = new GenerateWalletRequest { ScriptPubKeyType = ScriptPubKeyType.Segwit, SavePrivateKeys = true };
|
||||
|
||||
// var resp1 = await client.GenerateWalletAsync(req);
|
||||
// s.TestLogs.LogInformation($"Created hot wallet 1: {resp1.DerivationScheme} | {resp1.AccountKeyPath} | {resp1.MasterHDKey.ToWif()}");
|
||||
// var resp2 = await client.GenerateWalletAsync(req);
|
||||
// s.TestLogs.LogInformation($"Created hot wallet 2: {resp2.DerivationScheme} | {resp2.AccountKeyPath} | {resp2.MasterHDKey.ToWif()}");
|
||||
// var resp3 = await client.GenerateWalletAsync(req);
|
||||
// s.TestLogs.LogInformation($"Created hot wallet 3: {resp3.DerivationScheme} | {resp3.AccountKeyPath} | {resp3.MasterHDKey.ToWif()}");
|
||||
|
||||
var network = s.Server.NetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
|
||||
var resp1 = generateWalletResp("tprv8ZgxMBicQKsPeGSkDtxjScBmmHP4rfSEPkf1vNmoqt5QjPTco2zPd6UVWkJf2fU8gdKPYRdDMizxtMRqmpVpxsWuqRxVs2d5VsEhwxaK3h7",
|
||||
"57b3f43a/84'/1'/0'", "tpubDCzBHRPRcv7Y3utw1hZVrCar21gsj8vsXcehAG4z3R4NnmdMAASQwYYxGBd2f4q5s5ZFGvQBBFs1jVcGsXYoSTA1YFQPwizjsQLU12ibLyu", network);
|
||||
var resp2 = generateWalletResp("tprv8ZgxMBicQKsPeC6Xuw83UJHgjnszEUjwH9E5f5FZ3fHgJHBQApo8CmFCsowcdwbRM119UnTqSzVWUsWGtLsxc8wnZa5L8xmEsvEpiyRj4Js",
|
||||
"ee7d36c4/84'/1'/0'", "tpubDCetxnEjn8HXA5NrDZbKKTUUYoWCVC2V3X7Kmh3o9UYTfh9c3wTPKyCyeUrLkQ8KHYptEsBoQq6AgqPZiW5neEgb2kjKEr41q1qSevoPFDM", network);
|
||||
var resp3 = generateWalletResp("tprv8ZgxMBicQKsPekSniuKwLtXpB82dSDV8ZAK4uLUHxkiHWfDtR5yYwNZiicKdpT3UYwzTTMvXESCm45KyAiH7kiJY6yk51neC9ZvmwDpNsQh",
|
||||
"6c014fb3/84'/1'/0'", "tpubDCaTgjJfS5UEim6h66VpQBEZ2Tj6hHk8TzvL81HygdW1M8vZCRhUZLNhb3WTimyP2XMQRA3QGZPwwxUsEFQYK4EoRUWTcb9oB237FJ112tN", network);
|
||||
|
||||
var multisigDerivationScheme = $"wsh(multi(2,[{resp1.AccountKeyPath}]{resp1.DerivationScheme}/0/*," +
|
||||
$"[{resp2.AccountKeyPath}]{resp2.DerivationScheme}/0/*," +
|
||||
$"[{resp3.AccountKeyPath}]{resp3.DerivationScheme}/0/*))";
|
||||
|
||||
var strategy = UIStoresController.ParseDerivationStrategy(multisigDerivationScheme, network);
|
||||
strategy.Source = "ManualDerivationScheme";
|
||||
var derivationScheme = strategy.AccountDerivation;
|
||||
|
||||
s.GoToWalletSettings();
|
||||
s.Driver.FindElement(By.Id("ImportWalletOptionsLink")).Click();
|
||||
s.Driver.FindElement(By.Id("ImportXpubLink")).Click();
|
||||
s.Driver.FindElement(By.Id("DerivationScheme")).SendKeys(multisigDerivationScheme);
|
||||
s.Driver.FindElement(By.Id("Continue")).Click();
|
||||
s.Driver.FindElement(By.Id("Confirm")).Click();
|
||||
s.TestLogs.LogInformation($"Multisig wallet setup: {multisigDerivationScheme}");
|
||||
|
||||
// enabling multisig
|
||||
s.Driver.FindElement(By.Id("IsMultiSigOnServer")).Click();
|
||||
s.Driver.FindElement(By.Id("DefaultIncludeNonWitnessUtxo")).Click();
|
||||
s.Driver.FindElement(By.Id("SaveWalletSettings")).Click();
|
||||
Assert.Contains("Wallet settings successfully updated.", s.FindAlertMessage().Text);
|
||||
|
||||
// fetch address from receive page
|
||||
s.Driver.FindElement(By.Id("WalletNav-Receive")).Click();
|
||||
var address = s.Driver.FindElement(By.Id("Address")).GetAttribute("data-text");
|
||||
s.Driver.FindElement(By.XPath("//button[@value='fill-wallet']")).Click();
|
||||
s.Driver.FindElement(By.Id("CancelWizard")).Click();
|
||||
|
||||
// we are creating a pending transaction
|
||||
s.Driver.FindElement(By.Id("WalletNav-Send")).Click();
|
||||
s.Driver.FindElement(By.Id("Outputs_0__DestinationAddress")).SendKeys(address);
|
||||
var amount = "0.1";
|
||||
s.Driver.FindElement(By.Id("Outputs_0__Amount")).SendKeys(amount);
|
||||
s.Driver.FindElement(By.Id("CreatePendingTransaction")).Click();
|
||||
|
||||
// now clicking on View to sign transaction
|
||||
await SignPendingTransactionWithKey(s, address, derivationScheme, resp1);
|
||||
await SignPendingTransactionWithKey(s, address, derivationScheme, resp2);
|
||||
|
||||
// Broadcasting transaction and ensuring there is no longer broadcast button
|
||||
s.Driver.WaitForElement(By.XPath("//a[text()='Broadcast']")).Click();
|
||||
s.Driver.FindElement(By.Id("BroadcastTransaction")).Click();
|
||||
Assert.Contains("Transaction broadcasted successfully", s.FindAlertMessage().Text);
|
||||
s.Driver.AssertElementNotFound(By.XPath("//a[text()='Broadcast']"));
|
||||
|
||||
// Abort pending transaction flow
|
||||
s.Driver.FindElement(By.Id("WalletNav-Send")).Click();
|
||||
s.Driver.FindElement(By.Id("Outputs_0__DestinationAddress")).SendKeys(address);
|
||||
s.Driver.FindElement(By.Id("Outputs_0__Amount")).SendKeys("0.2");
|
||||
s.Driver.FindElement(By.Id("CreatePendingTransaction")).Click();
|
||||
|
||||
s.Driver.FindElement(By.XPath("//a[text()='Abort']")).Click();
|
||||
|
||||
s.Driver.FindElement(By.Id("ConfirmContinue")).Click();
|
||||
|
||||
Assert.Contains("Aborted Pending Transaction", s.FindAlertMessage().Text);
|
||||
|
||||
s.TestLogs.LogInformation($"Finished MultiSig Flow");
|
||||
}
|
||||
|
||||
private async Task SignPendingTransactionWithKey(SeleniumTester s, string address,
|
||||
DerivationStrategyBase derivationScheme, GenerateWalletResponse signingKey)
|
||||
{
|
||||
// getting to pending transaction page
|
||||
s.Driver.WaitForElement(By.XPath("//a[text()='View']")).Click();
|
||||
|
||||
var transactionRow = s.Driver.FindElement(By.XPath($"//tr[td[text()='{address}']]"));
|
||||
Assert.NotNull(transactionRow);
|
||||
|
||||
var signTransactionButton = s.Driver.FindElement(By.Id("SignTransaction"));
|
||||
Assert.NotNull(signTransactionButton);
|
||||
|
||||
// fetching PSBT
|
||||
s.Driver.FindElement(By.Id("PSBTOptionsExportHeader")).Click();
|
||||
s.Driver.WaitForElement(By.Id("ShowRawVersion")).Click();
|
||||
var psbt = s.Driver.WaitForElement(By.Id("psbt-base64")).Text;
|
||||
while (string.IsNullOrEmpty(psbt))
|
||||
{
|
||||
psbt = s.Driver.FindElement(By.Id("psbt-base64")).Text;
|
||||
}
|
||||
|
||||
// signing PSBT and entering it to submit
|
||||
var signedPsbt = SignWithSeed(psbt, derivationScheme, signingKey);
|
||||
|
||||
s.Driver.FindElement(By.Id("PSBTOptionsImportHeader")).Click();
|
||||
s.Driver.WaitForElement(By.Id("ImportedPSBT")).SendKeys(signedPsbt);
|
||||
|
||||
s.Driver.FindElement(By.Id("Decode")).Click();
|
||||
}
|
||||
|
||||
private GenerateWalletResponse generateWalletResp(string tpriv, string keypath, string derivation, BTCPayNetwork network)
|
||||
{
|
||||
var key1 = new BitcoinExtKey(
|
||||
ExtKey.Parse(tpriv, Network.RegTest),
|
||||
Network.RegTest);
|
||||
|
||||
|
||||
var parser = new DerivationSchemeParser(network);
|
||||
|
||||
var resp1 = new GenerateWalletResponse
|
||||
{
|
||||
MasterHDKey = key1,
|
||||
DerivationScheme = parser.Parse(derivation),
|
||||
AccountKeyPath = RootedKeyPath.Parse(keypath)
|
||||
};
|
||||
return resp1;
|
||||
}
|
||||
|
||||
|
||||
public string SignWithSeed(string psbtBase64, DerivationStrategyBase derivationStrategyBase,
|
||||
GenerateWalletResponse resp)
|
||||
{
|
||||
var strMasterHdKey = resp.MasterHDKey;
|
||||
var extKey = new BitcoinExtKey(strMasterHdKey, Network.RegTest);
|
||||
|
||||
var strKeypath = resp.AccountKeyPath.ToStringWithEmptyKeyPathAware();
|
||||
RootedKeyPath rootedKeyPath = RootedKeyPath.Parse(strKeypath);
|
||||
|
||||
|
||||
if (rootedKeyPath.MasterFingerprint != extKey.GetPublicKey().GetHDFingerPrint())
|
||||
throw new Exception("Master fingerprint mismatch. Ensure the wallet matches the PSBT.");
|
||||
// finished setting variables, now onto signing
|
||||
|
||||
var psbt = PSBT.Parse(psbtBase64, Network.RegTest);
|
||||
|
||||
// Sign the PSBT
|
||||
extKey = extKey.Derive(rootedKeyPath.KeyPath);
|
||||
psbt.Settings.SigningOptions = new SigningOptions();
|
||||
var changed = psbt.PSBTChanged(() => psbt.SignAll(derivationStrategyBase, extKey, rootedKeyPath));
|
||||
|
||||
if (!changed)
|
||||
throw new Exception("Failed to sign the PSBT. Ensure the inputs align with the account key path.");
|
||||
|
||||
// Return the updated and signed PSBT
|
||||
return psbt.ToBase64();
|
||||
}
|
||||
}
|
@ -20,9 +20,11 @@ using BTCPayServer.PayoutProcessors;
|
||||
using BTCPayServer.PayoutProcessors.Lightning;
|
||||
using BTCPayServer.Plugins.PointOfSale.Controllers;
|
||||
using BTCPayServer.Plugins.PointOfSale.Models;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Mails;
|
||||
using BTCPayServer.Services.Notifications;
|
||||
using BTCPayServer.Services.Notifications.Blobs;
|
||||
using BTCPayServer.Services.Stores;
|
||||
@ -1536,7 +1538,7 @@ namespace BTCPayServer.Tests
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanProcessPayoutsExternally()
|
||||
{
|
||||
@ -4091,6 +4093,45 @@ namespace BTCPayServer.Tests
|
||||
|
||||
await AssertAPIError("store-user-role-orphaned", async () => await employeeClient.RemoveStoreUser(user.StoreId, employee.UserId));
|
||||
}
|
||||
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task ServerEmailTests()
|
||||
{
|
||||
using var tester = CreateServerTester();
|
||||
await tester.StartAsync();
|
||||
var admin = tester.NewAccount();
|
||||
await admin.GrantAccessAsync(true);
|
||||
var adminClient = await admin.CreateClient(Policies.Unrestricted);
|
||||
var data = new EmailSettingsData
|
||||
{
|
||||
From = "admin@admin.com",
|
||||
Login = "admin@admin.com",
|
||||
Password = "admin@admin.com",
|
||||
Port = 1234,
|
||||
Server = "admin.com",
|
||||
};
|
||||
var serverEmailSettings = new ServerEmailSettingsData
|
||||
{
|
||||
EnableStoresToUseServerEmailSettings = false
|
||||
};
|
||||
await adminClient.UpdateServerEmailSettings(serverEmailSettings);
|
||||
|
||||
var s = await adminClient.GetServerEmailSettings();
|
||||
// email password is masked and not returned from the server once set
|
||||
serverEmailSettings.Password = null;
|
||||
Assert.Equal(JsonConvert.SerializeObject(s), JsonConvert.SerializeObject(serverEmailSettings));
|
||||
await AssertValidationError(new[] { nameof(EmailSettingsData.From) },
|
||||
async () => await adminClient.UpdateServerEmailSettings(new ServerEmailSettingsData
|
||||
{
|
||||
From = "invalid"
|
||||
}));
|
||||
|
||||
// NOTE: This email test fails silently in EmailSender.cs#31, can't test, but leaving for the future as reminder
|
||||
//await adminClient.SendEmail(admin.StoreId,
|
||||
// new SendEmailRequest { Body = "lol", Subject = "subj", Email = "to@example.org" });
|
||||
}
|
||||
|
||||
[Fact(Timeout = 60 * 2 * 1000)]
|
||||
[Trait("Integration", "Integration")]
|
||||
@ -4114,6 +4155,8 @@ namespace BTCPayServer.Tests
|
||||
};
|
||||
await adminClient.UpdateStoreEmailSettings(admin.StoreId, data);
|
||||
var s = await adminClient.GetStoreEmailSettings(admin.StoreId);
|
||||
// email password is masked and not returned from the server once set
|
||||
data.Password = null;
|
||||
Assert.Equal(JsonConvert.SerializeObject(s), JsonConvert.SerializeObject(data));
|
||||
await AssertValidationError(new[] { nameof(EmailSettingsData.From) },
|
||||
async () => await adminClient.UpdateStoreEmailSettings(admin.StoreId,
|
||||
@ -4824,9 +4867,9 @@ clientBasic.UpdateStoreRateConfiguration(user.StoreId, new StoreRateConfiguratio
|
||||
clientBasic.UpdateStoreRateConfiguration(user.StoreId, new StoreRateConfiguration() { IsCustomScript = false, PreferredSource = "coingeckoOOO", Spread = -1m }));
|
||||
|
||||
await AssertValidationError(new[] { "currencyPair" }, () =>
|
||||
clientBasic.PreviewUpdateStoreRateConfiguration(user.StoreId, new StoreRateConfiguration() { IsCustomScript = false, PreferredSource = "coingecko" }, new[] { "BTC_USD_USD_BTC" }));
|
||||
clientBasic.PreviewUpdateStoreRateConfiguration(user.StoreId, new StoreRateConfiguration() { IsCustomScript = false, PreferredSource = "coingecko" }, new[] { "BTCUSDUSDBTC" }));
|
||||
await AssertValidationError(new[] { "PreferredSource", "currencyPair" }, () =>
|
||||
clientBasic.PreviewUpdateStoreRateConfiguration(user.StoreId, new StoreRateConfiguration() { IsCustomScript = false, PreferredSource = "coingeckoOOO" }, new[] { "BTC_USD_USD_BTC" }));
|
||||
clientBasic.PreviewUpdateStoreRateConfiguration(user.StoreId, new StoreRateConfiguration() { IsCustomScript = false, PreferredSource = "coingeckoOOO" }, new[] { "BTCUSDUSDBTC" }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -426,7 +426,6 @@ namespace BTCPayServer.Tests
|
||||
var notifications = await nbx.CreateWebsocketNotificationSessionAsync();
|
||||
var alice = tester.NewAccount();
|
||||
await alice.RegisterDerivationSchemeAsync("BTC", ScriptPubKeyType.Segwit, true);
|
||||
await notifications.ListenDerivationSchemesAsync(new[] { alice.DerivationScheme });
|
||||
|
||||
BitcoinAddress address = null;
|
||||
for (int i = 0; i < 5; i++)
|
||||
@ -434,7 +433,7 @@ namespace BTCPayServer.Tests
|
||||
address = (await nbx.GetUnusedAsync(alice.DerivationScheme, DerivationFeature.Deposit)).Address;
|
||||
await tester.ExplorerNode.GenerateAsync(1);
|
||||
tester.ExplorerNode.SendToAddress(address, Money.Coins(1.0m));
|
||||
await notifications.NextEventAsync();
|
||||
await notifications.WaitReceive(alice.DerivationScheme);
|
||||
}
|
||||
var paymentAddress = new Key().PubKey.GetAddress(ScriptPubKeyType.Legacy, Network.RegTest);
|
||||
var otherAddress = new Key().PubKey.GetAddress(ScriptPubKeyType.Legacy, Network.RegTest);
|
||||
@ -569,10 +568,9 @@ namespace BTCPayServer.Tests
|
||||
|
||||
await notifications.DisposeAsync();
|
||||
notifications = await nbx.CreateWebsocketNotificationSessionAsync();
|
||||
await notifications.ListenDerivationSchemesAsync(new[] { bob.DerivationScheme });
|
||||
address = (await nbx.GetUnusedAsync(bob.DerivationScheme, DerivationFeature.Deposit)).Address;
|
||||
tester.ExplorerNode.SendToAddress(address, Money.Coins(1.1m));
|
||||
await notifications.NextEventAsync();
|
||||
await notifications.WaitReceive(bob.DerivationScheme);
|
||||
await bob.ModifyOnchainPaymentSettings(p => p.PayJoinEnabled = true);
|
||||
var invoice = bob.BitPay.CreateInvoice(
|
||||
new Invoice { Price = 0.1m, Currency = "BTC", FullNotifications = true });
|
||||
@ -603,7 +601,7 @@ namespace BTCPayServer.Tests
|
||||
proposal = proposal.SignAll(derivationSchemeSettings.AccountDerivation, alice.GenerateWalletResponseV.AccountHDKey, signingAccount.GetRootedKeyPath());
|
||||
proposal.Finalize();
|
||||
await tester.ExplorerNode.SendRawTransactionAsync(proposal.ExtractTransaction());
|
||||
await notifications.NextEventAsync();
|
||||
await notifications.WaitReceive(bob.DerivationScheme);
|
||||
|
||||
TestLogs.LogInformation("Abusing minFeeRate should give not enough money error");
|
||||
invoice = bob.BitPay.CreateInvoice(
|
||||
|
@ -32,6 +32,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using NBitcoin.Payment;
|
||||
using NBXplorer.Models;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using OpenQA.Selenium;
|
||||
@ -690,7 +691,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
// Store Emails without server fallback
|
||||
s.GoToStore(StoreNavPages.Emails);
|
||||
s.Driver.ElementDoesNotExist(By.Id("UseCustomSMTP"));
|
||||
s.Driver.ElementDoesNotExist(By.Id("IsCustomSMTP"));
|
||||
s.Driver.FindElement(By.Id("ConfigureEmailRules")).Click();
|
||||
Assert.Contains("You need to configure email settings before this feature works", s.Driver.PageSource);
|
||||
|
||||
@ -705,12 +706,12 @@ namespace BTCPayServer.Tests
|
||||
|
||||
// Store Emails with server fallback
|
||||
s.GoToStore(StoreNavPages.Emails);
|
||||
Assert.False(s.Driver.FindElement(By.Id("UseCustomSMTP")).Selected);
|
||||
Assert.False(s.Driver.FindElement(By.Id("IsCustomSMTP")).Selected);
|
||||
s.Driver.FindElement(By.Id("ConfigureEmailRules")).Click();
|
||||
Assert.DoesNotContain("You need to configure email settings before this feature works", s.Driver.PageSource);
|
||||
|
||||
s.GoToStore(StoreNavPages.Emails);
|
||||
s.Driver.FindElement(By.Id("UseCustomSMTP")).Click();
|
||||
s.Driver.FindElement(By.Id("IsCustomSMTP")).Click();
|
||||
Thread.Sleep(250);
|
||||
CanSetupEmailCore(s);
|
||||
|
||||
@ -731,7 +732,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.Contains("Store email rules saved", s.FindAlertMessage().Text);
|
||||
|
||||
s.GoToStore(StoreNavPages.Emails);
|
||||
Assert.True(s.Driver.FindElement(By.Id("UseCustomSMTP")).Selected);
|
||||
Assert.True(s.Driver.FindElement(By.Id("IsCustomSMTP")).Selected);
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
@ -1884,11 +1885,9 @@ namespace BTCPayServer.Tests
|
||||
|
||||
//send money to addr and ensure it changed
|
||||
var sess = await s.Server.ExplorerClient.CreateWebsocketNotificationSessionAsync();
|
||||
await sess.ListenAllTrackedSourceAsync();
|
||||
var nextEvent = sess.NextEventAsync();
|
||||
await s.Server.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create(receiveAddr, Network.RegTest),
|
||||
Money.Parse("0.1"));
|
||||
await nextEvent;
|
||||
await sess.WaitNext<NewTransactionEvent>(e => e.Outputs.FirstOrDefault()?.Address.ToString() == receiveAddr);
|
||||
await Task.Delay(200);
|
||||
s.Driver.Navigate().Refresh();
|
||||
s.Driver.FindElement(By.CssSelector("button[value=generate-new-address]")).Click();
|
||||
@ -1898,6 +1897,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
// Check the label is applied to the tx
|
||||
s.Driver.WaitWalletTransactionsLoaded();
|
||||
// Sometimes this fails in local, but not CI
|
||||
Assert.Equal("label2", s.Driver.FindElement(By.XPath("//*[@id=\"WalletTransactionsList\"]//*[contains(@class, 'transaction-label')]")).Text);
|
||||
|
||||
//change the wallet and ensure old address is not there and generating a new one does not result in the prev one
|
||||
@ -1960,7 +1960,7 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.FindElement(By.Id("SignTransaction")).Click();
|
||||
// Back button should lead back to the previous page inside the send wizard
|
||||
var backUrl = s.Driver.FindElement(By.Id("GoBack")).GetAttribute("href");
|
||||
Assert.EndsWith($"/send?returnUrl={walletTransactionUri.AbsolutePath}", backUrl);
|
||||
Assert.EndsWith($"/send?returnUrl={Uri.EscapeDataString(walletTransactionUri.AbsolutePath)}", backUrl);
|
||||
// Cancel button should lead to the page that referred to the send wizard
|
||||
var cancelUrl = s.Driver.FindElement(By.Id("CancelWizard")).GetAttribute("href");
|
||||
Assert.EndsWith(walletTransactionUri.AbsolutePath, cancelUrl);
|
||||
|
@ -967,7 +967,6 @@ namespace BTCPayServer.Tests
|
||||
using (var cts = new CancellationTokenSource(10000))
|
||||
using (var listener = tester.ExplorerClient.CreateWebsocketNotificationSession())
|
||||
{
|
||||
listener.ListenAllDerivationSchemes();
|
||||
var replaced = tester.ExplorerNode.SignRawTransaction(tx);
|
||||
Thread.Sleep(1000); // Make sure the replacement has a different timestamp
|
||||
var tx2 = tester.ExplorerNode.SendRawTransaction(replaced);
|
||||
|
@ -98,7 +98,7 @@ services:
|
||||
custom:
|
||||
|
||||
nbxplorer:
|
||||
image: nicolasdorier/nbxplorer:2.5.16
|
||||
image: nicolasdorier/nbxplorer:2.5.22
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "32838:32838"
|
||||
@ -229,7 +229,7 @@ services:
|
||||
- "5432"
|
||||
|
||||
merchant_lnd:
|
||||
image: btcpayserver/lnd:v0.18.4-beta
|
||||
image: btcpayserver/lnd:v0.18.5-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
@ -267,7 +267,7 @@ services:
|
||||
- bitcoind
|
||||
|
||||
customer_lnd:
|
||||
image: btcpayserver/lnd:v0.18.4-beta
|
||||
image: btcpayserver/lnd:v0.18.5-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
|
@ -62,7 +62,7 @@ services:
|
||||
custom:
|
||||
|
||||
nbxplorer:
|
||||
image: nicolasdorier/nbxplorer:2.5.16
|
||||
image: nicolasdorier/nbxplorer:2.5.22
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "32838:32838"
|
||||
@ -187,7 +187,7 @@ services:
|
||||
- "5432"
|
||||
|
||||
merchant_lnd:
|
||||
image: btcpayserver/lnd:v0.18.4-beta
|
||||
image: btcpayserver/lnd:v0.18.5-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
@ -225,7 +225,7 @@ services:
|
||||
- bitcoind
|
||||
|
||||
customer_lnd:
|
||||
image: btcpayserver/lnd:v0.18.4-beta
|
||||
image: btcpayserver/lnd:v0.18.5-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
|
@ -57,7 +57,7 @@ services:
|
||||
custom:
|
||||
|
||||
nbxplorer:
|
||||
image: nicolasdorier/nbxplorer:2.5.16
|
||||
image: nicolasdorier/nbxplorer:2.5.22
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "32838:32838"
|
||||
@ -177,7 +177,7 @@ services:
|
||||
- "5432"
|
||||
|
||||
merchant_lnd:
|
||||
image: btcpayserver/lnd:v0.18.4-beta
|
||||
image: btcpayserver/lnd:v0.18.5-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
@ -215,7 +215,7 @@ services:
|
||||
- bitcoind
|
||||
|
||||
customer_lnd:
|
||||
image: btcpayserver/lnd:v0.18.4-beta
|
||||
image: btcpayserver/lnd:v0.18.5-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
|
@ -95,7 +95,7 @@ services:
|
||||
custom:
|
||||
|
||||
nbxplorer:
|
||||
image: nicolasdorier/nbxplorer:2.5.16
|
||||
image: nicolasdorier/nbxplorer:2.5.22
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "32838:32838"
|
||||
@ -215,7 +215,7 @@ services:
|
||||
- "5432"
|
||||
|
||||
merchant_lnd:
|
||||
image: btcpayserver/lnd:v0.18.4-beta
|
||||
image: btcpayserver/lnd:v0.18.5-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
@ -253,7 +253,7 @@ services:
|
||||
- bitcoind
|
||||
|
||||
customer_lnd:
|
||||
image: btcpayserver/lnd:v0.18.4-beta
|
||||
image: btcpayserver/lnd:v0.18.5-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
|
@ -44,11 +44,11 @@
|
||||
@foreach (var tx in Model.Transactions)
|
||||
{
|
||||
<tr>
|
||||
<td>@tx.Timestamp.ToTimeAgo()</td>
|
||||
<td>
|
||||
<td class="align-middle">@tx.Timestamp.ToTimeAgo()</td>
|
||||
<td class="align-middle">
|
||||
<vc:truncate-center text="@tx.Id" link="@tx.Link" classes="truncate-center-id" />
|
||||
</td>
|
||||
<td>
|
||||
<td class="align-middle">
|
||||
@if (tx.Labels.Any())
|
||||
{
|
||||
<div class="d-flex flex-wrap gap-2 align-items-center">
|
||||
@ -70,18 +70,9 @@
|
||||
}
|
||||
|
||||
</td>
|
||||
@if (tx.Positive)
|
||||
{
|
||||
<td class="text-end text-success">
|
||||
<span data-sensitive>@DisplayFormatter.Currency(tx.Balance, tx.Currency)</span>
|
||||
</td>
|
||||
}
|
||||
else
|
||||
{
|
||||
<td class="text-end text-danger">
|
||||
<span data-sensitive>@DisplayFormatter.Currency(tx.Balance, tx.Currency)</span>
|
||||
</td>
|
||||
}
|
||||
<td class="align-middle text-end">
|
||||
<span data-sensitive class="text-@(tx.Positive ? "success" : "danger")@(tx.IsConfirmed ? "" : " opacity-50")">@DisplayFormatter.Currency(tx.Balance, tx.Currency)</span>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
|
@ -0,0 +1,89 @@
|
||||
#nullable enable
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Models.ServerViewModels;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Mails;
|
||||
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 GreenfieldServerEmailController : Controller
|
||||
{
|
||||
private readonly EmailSenderFactory _emailSenderFactory;
|
||||
private readonly PoliciesSettings _policiesSettings;
|
||||
readonly SettingsRepository _settingsRepository;
|
||||
|
||||
public GreenfieldServerEmailController(EmailSenderFactory emailSenderFactory, PoliciesSettings policiesSettings, SettingsRepository settingsRepository)
|
||||
{
|
||||
_emailSenderFactory = emailSenderFactory;
|
||||
_policiesSettings = policiesSettings;
|
||||
_settingsRepository = settingsRepository;
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyServerSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpGet("~/api/v1/server/email")]
|
||||
public async Task<IActionResult> ServerEmailSettings()
|
||||
{
|
||||
var email = await _emailSenderFactory.GetSettings() ?? new EmailSettings();
|
||||
var model = new ServerEmailSettingsData
|
||||
{
|
||||
EnableStoresToUseServerEmailSettings = !_policiesSettings.DisableStoresToUseServerEmailSettings,
|
||||
From = email.From,
|
||||
Server = email.Server,
|
||||
Port = email.Port,
|
||||
Login = email.Login,
|
||||
DisableCertificateCheck = email.DisableCertificateCheck,
|
||||
// Password is not returned
|
||||
Password = null
|
||||
};
|
||||
return Ok(model);
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyServerSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpPut("~/api/v1/server/email")]
|
||||
public async Task<IActionResult> ServerEmailSettings(ServerEmailSettingsData request)
|
||||
{
|
||||
if (_policiesSettings.DisableStoresToUseServerEmailSettings == request.EnableStoresToUseServerEmailSettings)
|
||||
{
|
||||
_policiesSettings.DisableStoresToUseServerEmailSettings = !request.EnableStoresToUseServerEmailSettings;
|
||||
await _settingsRepository.UpdateSetting(_policiesSettings);
|
||||
}
|
||||
|
||||
// save
|
||||
if (request.From is not null && !MailboxAddressValidator.IsMailboxAddress(request.From))
|
||||
{
|
||||
request.AddModelError(e => e.From,
|
||||
"Invalid email address", this);
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
|
||||
var oldSettings = await _emailSenderFactory.GetSettings() ?? new EmailSettings();
|
||||
// retaining the password if it exists and was not provided in request
|
||||
if (string.IsNullOrEmpty(request.Password) &&
|
||||
!string.IsNullOrEmpty(oldSettings?.Password))
|
||||
request.Password = oldSettings.Password;
|
||||
|
||||
// important to save as EmailSettings otherwise it won't be able to be fetched
|
||||
await _settingsRepository.UpdateSetting(new EmailSettings
|
||||
{
|
||||
Server = request.Server,
|
||||
Port = request.Port,
|
||||
Login = request.Login,
|
||||
Password = request.Password,
|
||||
From = request.From,
|
||||
DisableCertificateCheck = request.DisableCertificateCheck
|
||||
});
|
||||
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
}
|
@ -55,7 +55,6 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
[HttpGet("~/api/v1/stores/{storeId}/email")]
|
||||
public IActionResult GetStoreEmailSettings()
|
||||
{
|
||||
|
||||
var store = HttpContext.GetStoreData();
|
||||
return store == null ? StoreNotFound() : Ok(FromModel(store));
|
||||
}
|
||||
@ -76,7 +75,13 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
"Invalid email address", this);
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
|
||||
var blob = store.GetStoreBlob();
|
||||
|
||||
// retaining the password if it exists and was not provided in request
|
||||
if (string.IsNullOrEmpty(request.Password) && blob.EmailSettings?.Password != null)
|
||||
request.Password = blob.EmailSettings?.Password;
|
||||
|
||||
blob.EmailSettings = request;
|
||||
if (store.SetStoreBlob(blob))
|
||||
{
|
||||
@ -87,7 +92,11 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
}
|
||||
private EmailSettings FromModel(Data.StoreData data)
|
||||
{
|
||||
return data.GetStoreBlob().EmailSettings ?? new();
|
||||
var emailSettings = data.GetStoreBlob().EmailSettings;
|
||||
if (emailSettings == null)
|
||||
return new EmailSettings();
|
||||
emailSettings.Password = null;
|
||||
return emailSettings;
|
||||
}
|
||||
private IActionResult StoreNotFound()
|
||||
{
|
||||
|
@ -782,22 +782,13 @@ namespace BTCPayServer
|
||||
DescriptionHashOnly = true
|
||||
};
|
||||
invoice = await client.CreateInvoice(param);
|
||||
if (!BOLT11PaymentRequest.Parse(invoice.BOLT11, network.NBitcoinNetwork)
|
||||
.VerifyDescriptionHash(description))
|
||||
{
|
||||
return BadRequest(new LNUrlStatusResponse
|
||||
{
|
||||
Status = "ERROR",
|
||||
Reason = "Lightning node could not generate invoice with a valid description hash"
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BadRequest(new LNUrlStatusResponse
|
||||
{
|
||||
Status = "ERROR",
|
||||
Reason = "Lightning node could not generate invoice with description hash" + (
|
||||
Reason = "Lightning node could not generate invoice" + (
|
||||
string.IsNullOrEmpty(ex.Message) ? "" : $": {ex.Message}")
|
||||
});
|
||||
}
|
||||
|
@ -1263,10 +1263,8 @@ namespace BTCPayServer.Controllers
|
||||
return View(model);
|
||||
}
|
||||
var oldSettings = await _emailSenderFactory.GetSettings() ?? new EmailSettings();
|
||||
if (new ServerEmailsViewModel(oldSettings).PasswordSet)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(oldSettings.Password))
|
||||
model.Settings.Password = oldSettings.Password;
|
||||
}
|
||||
|
||||
await _SettingsRepository.UpdateSetting(model.Settings);
|
||||
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Email settings saved"].Value;
|
||||
|
@ -47,7 +47,7 @@ public partial class UIStoresController
|
||||
{
|
||||
vm.Rules ??= [];
|
||||
int commandIndex = 0;
|
||||
|
||||
|
||||
var indSep = command.Split(':', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (indSep.Length > 1)
|
||||
{
|
||||
@ -154,15 +154,15 @@ public partial class UIStoresController
|
||||
{
|
||||
[Required]
|
||||
public string Trigger { get; set; }
|
||||
|
||||
|
||||
public bool CustomerEmail { get; set; }
|
||||
|
||||
|
||||
|
||||
|
||||
public string To { get; set; }
|
||||
|
||||
|
||||
[Required]
|
||||
public string Subject { get; set; }
|
||||
|
||||
|
||||
[Required]
|
||||
public string Body { get; set; }
|
||||
}
|
||||
@ -174,51 +174,65 @@ public partial class UIStoresController
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
|
||||
var emailSender = await _emailSenderFactory.GetEmailSender(store.Id);
|
||||
var data = await emailSender.GetEmailSettings() ?? new EmailSettings();
|
||||
var fallbackSettings = emailSender is StoreEmailSender { FallbackSender: { } fallbackSender }
|
||||
? await fallbackSender.GetEmailSettings()
|
||||
: null;
|
||||
var settings = data != fallbackSettings ? data : new EmailSettings();
|
||||
return View(new EmailsViewModel(settings, fallbackSettings));
|
||||
var settings = await GetCustomSettings(store.Id);
|
||||
|
||||
return View(new EmailsViewModel(settings.Custom ?? new())
|
||||
{
|
||||
IsFallbackSetup = settings.Fallback is not null,
|
||||
IsCustomSMTP = settings.Custom is not null || settings.Fallback is null
|
||||
});
|
||||
}
|
||||
|
||||
record AllEmailSettings(EmailSettings Custom, EmailSettings Fallback);
|
||||
private async Task<AllEmailSettings> GetCustomSettings(string storeId)
|
||||
{
|
||||
var sender = await _emailSenderFactory.GetEmailSender(storeId) as StoreEmailSender;
|
||||
if (sender is null)
|
||||
return new(null, null);
|
||||
var fallback = sender.FallbackSender is { } fb ? await fb.GetEmailSettings() : null;
|
||||
if (fallback?.IsComplete() is not true)
|
||||
fallback = null;
|
||||
return new(await sender.GetCustomSettings(), fallback);
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/email-settings")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> StoreEmailSettings(string storeId, EmailsViewModel model, string command, [FromForm] bool useCustomSMTP = false)
|
||||
public async Task<IActionResult> StoreEmailSettings(string storeId, EmailsViewModel model, string command)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
|
||||
model.FallbackSettings = await _emailSenderFactory.GetEmailSender(store.Id) is StoreEmailSender { FallbackSender: not null } storeSender
|
||||
? await storeSender.FallbackSender.GetEmailSettings()
|
||||
: null;
|
||||
if (model.FallbackSettings is null) useCustomSMTP = true;
|
||||
ViewBag.UseCustomSMTP = useCustomSMTP;
|
||||
if (useCustomSMTP)
|
||||
var settings = await GetCustomSettings(store.Id);
|
||||
model.IsFallbackSetup = settings.Fallback is not null;
|
||||
if (!model.IsFallbackSetup)
|
||||
model.IsCustomSMTP = true;
|
||||
if (model.IsCustomSMTP)
|
||||
{
|
||||
model.Settings.Validate("Settings.", ModelState);
|
||||
if (model.Settings.From is not null && !MailboxAddressValidator.IsMailboxAddress(model.Settings.From))
|
||||
{
|
||||
ModelState.AddModelError("Settings.From", StringLocalizer["Invalid email"]);
|
||||
}
|
||||
if (!ModelState.IsValid)
|
||||
return View(model);
|
||||
}
|
||||
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var currentSettings = store.GetStoreBlob().EmailSettings;
|
||||
if (model is { IsCustomSMTP: true, Settings: { Password: null } })
|
||||
model.Settings.Password = currentSettings?.Password;
|
||||
|
||||
if (command == "Test")
|
||||
{
|
||||
try
|
||||
{
|
||||
if (useCustomSMTP)
|
||||
{
|
||||
if (model.PasswordSet)
|
||||
{
|
||||
model.Settings.Password = store.GetStoreBlob().EmailSettings.Password;
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(model.TestEmail))
|
||||
ModelState.AddModelError(nameof(model.TestEmail), new RequiredAttribute().FormatErrorMessage(nameof(model.TestEmail)));
|
||||
if (!ModelState.IsValid)
|
||||
return View(model);
|
||||
var settings = useCustomSMTP ? model.Settings : model.FallbackSettings;
|
||||
using var client = await settings.CreateSmtpClient();
|
||||
var message = settings.CreateMailMessage(MailboxAddress.Parse(model.TestEmail), $"{store.StoreName}: Email test", StringLocalizer["You received it, the BTCPay Server SMTP settings work."], false);
|
||||
var clientSettings = (model.IsCustomSMTP ? model.Settings : settings.Fallback) ?? new();
|
||||
using var client = await clientSettings.CreateSmtpClient();
|
||||
var message = clientSettings.CreateMailMessage(MailboxAddress.Parse(model.TestEmail), $"{store.StoreName}: Email test", StringLocalizer["You received it, the BTCPay Server SMTP settings work."], false);
|
||||
await client.SendAsync(message);
|
||||
await client.DisconnectAsync(true);
|
||||
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Email sent to {0}. Please verify you received it.", model.TestEmail].Value;
|
||||
@ -229,29 +243,24 @@ public partial class UIStoresController
|
||||
}
|
||||
return View(model);
|
||||
}
|
||||
if (command == "ResetPassword")
|
||||
else if (command == "ResetPassword")
|
||||
{
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
storeBlob.EmailSettings.Password = null;
|
||||
if (storeBlob.EmailSettings is not null)
|
||||
storeBlob.EmailSettings.Password = null;
|
||||
store.SetStoreBlob(storeBlob);
|
||||
await _storeRepo.UpdateStore(store);
|
||||
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Email server password reset"].Value;
|
||||
}
|
||||
var unsetCustomSMTP = !useCustomSMTP && store.GetStoreBlob().EmailSettings is not null;
|
||||
if (useCustomSMTP || unsetCustomSMTP)
|
||||
else if (!model.IsCustomSMTP && currentSettings is not null)
|
||||
{
|
||||
if (model.Settings.From is not null && !MailboxAddressValidator.IsMailboxAddress(model.Settings.From))
|
||||
{
|
||||
ModelState.AddModelError("Settings.From", StringLocalizer["Invalid email"]);
|
||||
}
|
||||
if (!ModelState.IsValid)
|
||||
return View(model);
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
if (storeBlob.EmailSettings != null && new EmailsViewModel(storeBlob.EmailSettings, model.FallbackSettings).PasswordSet)
|
||||
{
|
||||
model.Settings.Password = storeBlob.EmailSettings.Password;
|
||||
}
|
||||
storeBlob.EmailSettings = unsetCustomSMTP ? null : model.Settings;
|
||||
storeBlob.EmailSettings = null;
|
||||
store.SetStoreBlob(storeBlob);
|
||||
await _storeRepo.UpdateStore(store);
|
||||
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["You are now using server's email settings"].Value;
|
||||
}
|
||||
else if (model.IsCustomSMTP)
|
||||
{
|
||||
storeBlob.EmailSettings = model.Settings;
|
||||
store.SetStoreBlob(storeBlob);
|
||||
await _storeRepo.UpdateStore(store);
|
||||
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Email settings modified"].Value;
|
||||
|
@ -789,7 +789,7 @@ public partial class UIStoresController
|
||||
$"The store won't be able to receive {cryptoCode} onchain payments until a new wallet is set up.");
|
||||
}
|
||||
|
||||
private DerivationSchemeSettings ParseDerivationStrategy(string derivationScheme, BTCPayNetwork network)
|
||||
internal static DerivationSchemeSettings ParseDerivationStrategy(string derivationScheme, BTCPayNetwork network)
|
||||
{
|
||||
var parser = new DerivationSchemeParser(network);
|
||||
var isOD = Regex.Match(derivationScheme, @"\(.*?\)");
|
||||
|
@ -239,6 +239,7 @@ namespace BTCPayServer.Controllers
|
||||
vm.NBXSeedAvailable = await CanUseHotWallet() && derivationSchemeSettings.IsHotWallet;
|
||||
vm.BackUrl ??= HttpContext.Request.GetTypedHeaders().Referer?.AbsolutePath;
|
||||
|
||||
vm.SigningContext.PSBT = vm.PSBT;
|
||||
var psbt = await vm.GetPSBT(network.NBitcoinNetwork, ModelState);
|
||||
if (vm.InvalidPSBT)
|
||||
{
|
||||
@ -259,6 +260,18 @@ namespace BTCPayServer.Controllers
|
||||
ModelState.Remove(nameof(vm.PSBT));
|
||||
ModelState.Remove(nameof(vm.FileName));
|
||||
ModelState.Remove(nameof(vm.UploadedPSBTFile));
|
||||
|
||||
// for pending transactions we collect signature from PSBT and redirect if everything is good
|
||||
if (vm.SigningContext.PendingTransactionId is not null)
|
||||
{
|
||||
return await RedirectToWalletPSBTReady(walletId,
|
||||
new WalletPSBTReadyViewModel
|
||||
{
|
||||
SigningContext = vm.SigningContext, ReturnUrl = vm.ReturnUrl, BackUrl = vm.BackUrl
|
||||
});
|
||||
}
|
||||
|
||||
// for regular transactions we decode PSBT and show the details
|
||||
await FetchTransactionDetails(walletId, derivationSchemeSettings, vm, network);
|
||||
return View("WalletPSBTDecoded", vm);
|
||||
|
||||
@ -603,16 +616,18 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Transaction broadcasted successfully ({0})", transaction.GetHash()].Value;
|
||||
}
|
||||
if (!string.IsNullOrEmpty(vm.ReturnUrl))
|
||||
{
|
||||
return LocalRedirect(vm.ReturnUrl);
|
||||
}
|
||||
|
||||
if (vm.SigningContext.PendingTransactionId is not null)
|
||||
{
|
||||
await _pendingTransactionService.Broadcasted(walletId.CryptoCode, walletId.StoreId,
|
||||
vm.SigningContext.PendingTransactionId);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(vm.ReturnUrl))
|
||||
{
|
||||
return LocalRedirect(vm.ReturnUrl);
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(WalletTransactions), new { walletId = walletId.ToString() });
|
||||
}
|
||||
case "analyze-psbt":
|
||||
|
@ -462,7 +462,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
await cashCow.SendCommandAsync("rescanblockchain");
|
||||
}
|
||||
var addresses = Enumerable.Range(0, 200).Select(_ => c.GetUnusedAsync(paymentMethod.AccountDerivation, DerivationFeature.Deposit, reserve: true)).ToArray();
|
||||
var addresses = Enumerable.Range(0, 10).Select(_ => c.GetUnusedAsync(paymentMethod.AccountDerivation, DerivationFeature.Deposit, reserve: true)).ToArray();
|
||||
|
||||
await Task.WhenAll(addresses);
|
||||
await cashCow.GenerateAsync(addresses.Length / 8);
|
||||
|
@ -12,6 +12,10 @@ namespace Microsoft.AspNetCore.Mvc
|
||||
public static class UrlHelperExtensions
|
||||
{
|
||||
#nullable enable
|
||||
public static string? WalletSend(this IUrlHelper helper, WalletId walletId) => helper.Action(nameof(UIWalletsController.WalletSend), new { walletId });
|
||||
public static string? WalletTransactions(this IUrlHelper helper, string walletId) => WalletTransactions(helper, WalletId.Parse(walletId));
|
||||
public static string? WalletTransactions(this IUrlHelper helper, WalletId walletId)
|
||||
=> helper.Action(nameof(UIWalletsController.WalletTransactions), new { walletId });
|
||||
public static Uri ActionAbsolute(this IUrlHelper helper, HttpRequest request, string? action, string? controller, object? values)
|
||||
=> request.GetAbsoluteUriNoPathBase(new Uri(helper.Action(action, controller, values) ?? "", UriKind.Relative));
|
||||
public static Uri ActionAbsolute(this IUrlHelper helper, HttpRequest request, string? action, string? controller)
|
||||
|
@ -11,13 +11,9 @@ public class HtmlInputFormProvider : FormComponentProviderBase
|
||||
{
|
||||
foreach (var t in new[] {
|
||||
"text",
|
||||
"radio",
|
||||
"checkbox",
|
||||
"password",
|
||||
"file",
|
||||
"hidden",
|
||||
"button",
|
||||
"submit",
|
||||
"color",
|
||||
"date",
|
||||
"datetime-local",
|
||||
@ -25,13 +21,9 @@ public class HtmlInputFormProvider : FormComponentProviderBase
|
||||
"week",
|
||||
"time",
|
||||
"email",
|
||||
"image",
|
||||
"number",
|
||||
"range",
|
||||
"search",
|
||||
"url",
|
||||
"tel",
|
||||
"reset"})
|
||||
"tel"})
|
||||
typeToComponentProvider.Add(t, this);
|
||||
}
|
||||
public override string View => "Forms/InputElement";
|
||||
|
@ -123,12 +123,7 @@ public class PendingTransactionService(
|
||||
await using var ctx = dbContextFactory.CreateContext();
|
||||
var pendingTransaction =
|
||||
await ctx.PendingTransactions.FindAsync(new object[] { cryptoCode, txId.ToString() }, cancellationToken);
|
||||
if (pendingTransaction is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (pendingTransaction.State != PendingTransactionState.Pending)
|
||||
if (pendingTransaction is null || pendingTransaction.State != PendingTransactionState.Pending)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
@ -138,21 +133,43 @@ public class PendingTransactionService(
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var originalPsbtWorkingCopy = PSBT.Parse(blob.PSBT, network.NBitcoinNetwork);
|
||||
|
||||
// Deduplicate: Check if this exact PSBT (Base64) was already collected
|
||||
var newPsbtBase64 = psbt.ToBase64();
|
||||
if (blob.CollectedSignatures.Any(s => s.ReceivedPSBT == newPsbtBase64))
|
||||
{
|
||||
return pendingTransaction; // Avoid duplicate signature collection
|
||||
}
|
||||
|
||||
foreach (var collectedSignature in blob.CollectedSignatures)
|
||||
{
|
||||
var collectedPsbt = PSBT.Parse(collectedSignature.ReceivedPSBT, network.NBitcoinNetwork);
|
||||
originalPsbtWorkingCopy = originalPsbtWorkingCopy.Combine(collectedPsbt);
|
||||
originalPsbtWorkingCopy.Combine(collectedPsbt); // combine changes the object
|
||||
}
|
||||
|
||||
var originalPsbtWorkingCopyWithNewPsbt = originalPsbtWorkingCopy.Combine(psbt);
|
||||
//check if we have more signatures than before
|
||||
if (originalPsbtWorkingCopyWithNewPsbt.Inputs.All(i =>
|
||||
i.PartialSigs.Count >= originalPsbtWorkingCopy.Inputs[(int)i.Index].PartialSigs.Count))
|
||||
var originalPsbtWorkingCopyWithNewPsbt = originalPsbtWorkingCopy.Clone(); // Clone before modifying
|
||||
originalPsbtWorkingCopyWithNewPsbt.Combine(psbt);
|
||||
|
||||
// Check if new signatures were actually added
|
||||
bool newSignaturesCollected = false;
|
||||
for (int i = 0; i < originalPsbtWorkingCopy.Inputs.Count; i++)
|
||||
{
|
||||
if (originalPsbtWorkingCopyWithNewPsbt.Inputs[i].PartialSigs.Count >
|
||||
originalPsbtWorkingCopy.Inputs[i].PartialSigs.Count)
|
||||
{
|
||||
newSignaturesCollected = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (newSignaturesCollected)
|
||||
{
|
||||
blob.CollectedSignatures.Add(new CollectedSignature
|
||||
{
|
||||
ReceivedPSBT = psbt.ToBase64(), Timestamp = DateTimeOffset.UtcNow
|
||||
ReceivedPSBT = newPsbtBase64,
|
||||
Timestamp = DateTimeOffset.UtcNow
|
||||
});
|
||||
pendingTransaction.SetBlob(blob);
|
||||
}
|
||||
@ -163,6 +180,7 @@ public class PendingTransactionService(
|
||||
}
|
||||
|
||||
await ctx.SaveChangesAsync(cancellationToken);
|
||||
|
||||
if (broadcastIfComplete && pendingTransaction.State == PendingTransactionState.Signed)
|
||||
{
|
||||
var explorerClient = explorerClientProvider.GetExplorerClient(network);
|
||||
@ -182,6 +200,8 @@ public class PendingTransactionService(
|
||||
return pendingTransaction;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public async Task<PendingTransaction?> GetPendingTransaction(string cryptoCode, string storeId, string txId)
|
||||
{
|
||||
await using var ctx = dbContextFactory.CreateContext();
|
||||
|
@ -7,9 +7,8 @@ namespace BTCPayServer.Models;
|
||||
public class EmailsViewModel
|
||||
{
|
||||
public EmailSettings Settings { get; set; }
|
||||
public EmailSettings FallbackSettings { get; set; }
|
||||
public bool PasswordSet { get; set; }
|
||||
|
||||
|
||||
[MailboxAddress]
|
||||
[Display(Name = "Test Email")]
|
||||
public string TestEmail { get; set; }
|
||||
@ -18,14 +17,14 @@ public class EmailsViewModel
|
||||
{
|
||||
}
|
||||
|
||||
public EmailsViewModel(EmailSettings settings, EmailSettings fallbackSettings = null)
|
||||
public EmailsViewModel(EmailSettings settings)
|
||||
{
|
||||
Settings = settings;
|
||||
FallbackSettings = fallbackSettings;
|
||||
PasswordSet = !string.IsNullOrEmpty(settings?.Password);
|
||||
PasswordSet = !string.IsNullOrWhiteSpace(settings?.Password);
|
||||
}
|
||||
|
||||
|
||||
public bool IsSetup() => Settings?.IsComplete() is true;
|
||||
public bool IsFallbackSetup() => FallbackSettings?.IsComplete() is true;
|
||||
public bool UsesFallback() => IsFallbackSetup() && Settings == FallbackSettings;
|
||||
|
||||
public bool IsFallbackSetup { get; set; }
|
||||
public bool IsCustomSMTP { get; set; }
|
||||
}
|
||||
|
25
BTCPayServer/Models/IHasBackAndReturnUrl.cs
Normal file
25
BTCPayServer/Models/IHasBackAndReturnUrl.cs
Normal file
@ -0,0 +1,25 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using BTCPayServer.Controllers;
|
||||
|
||||
namespace BTCPayServer.Models
|
||||
{
|
||||
public interface IHasBackAndReturnUrl
|
||||
{
|
||||
string? BackUrl { get; set; }
|
||||
string? ReturnUrl { get; set; }
|
||||
(string? backUrl, string? returnUrl) NormalizeBackAndReturnUrl()
|
||||
{
|
||||
var backUrl = BackUrl;
|
||||
if (backUrl is not null && ReturnUrl is not null)
|
||||
{
|
||||
var queryParam = $"returnUrl={Uri.EscapeDataString(ReturnUrl)}";
|
||||
if (backUrl.Contains('?'))
|
||||
backUrl = $"{backUrl}&{queryParam}";
|
||||
else
|
||||
backUrl = $"{backUrl}?{queryParam}";
|
||||
}
|
||||
return (backUrl, ReturnUrl);
|
||||
}
|
||||
}
|
||||
}
|
@ -4,7 +4,7 @@ using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Models.WalletViewModels
|
||||
{
|
||||
public class SignWithSeedViewModel
|
||||
public class SignWithSeedViewModel : IHasBackAndReturnUrl
|
||||
{
|
||||
public SigningContextModel SigningContext { get; set; } = new SigningContextModel();
|
||||
|
||||
|
@ -7,7 +7,7 @@ using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Models.WalletViewModels
|
||||
{
|
||||
public class WalletPSBTCombineViewModel
|
||||
public class WalletPSBTCombineViewModel : IHasBackAndReturnUrl
|
||||
{
|
||||
public string OtherPSBT { get; set; }
|
||||
[Display(Name = "PSBT to combine with…")]
|
||||
|
@ -5,7 +5,7 @@ using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Models.WalletViewModels
|
||||
{
|
||||
public class WalletPSBTReadyViewModel
|
||||
public class WalletPSBTReadyViewModel : IHasBackAndReturnUrl
|
||||
{
|
||||
public SigningContextModel SigningContext { get; set; } = new SigningContextModel();
|
||||
public string SigningKey { get; set; }
|
||||
@ -27,6 +27,12 @@ namespace BTCPayServer.Models.WalletViewModels
|
||||
public string BalanceChange { get; set; }
|
||||
public IEnumerable<TransactionTagModel> Labels { get; set; } = new List<TransactionTagModel>();
|
||||
}
|
||||
public class AmountViewModel
|
||||
{
|
||||
public bool Positive { get; set; }
|
||||
public string BalanceChange { get; set; }
|
||||
}
|
||||
public AmountViewModel ReplacementBalanceChange { get; set; }
|
||||
public bool HasErrors => Inputs.Count == 0 || Inputs.Any(i => !string.IsNullOrEmpty(i.Error));
|
||||
public string BalanceChange { get; set; }
|
||||
public bool CanCalculateBalance { get; set; }
|
||||
|
@ -5,7 +5,7 @@ using BTCPayServer.Services.Labels;
|
||||
|
||||
namespace BTCPayServer.Models.WalletViewModels
|
||||
{
|
||||
public class WalletSendModel
|
||||
public class WalletSendModel : IHasBackAndReturnUrl
|
||||
{
|
||||
public enum ThreeStateBool
|
||||
{
|
||||
|
@ -1,6 +1,6 @@
|
||||
namespace BTCPayServer.Models.WalletViewModels
|
||||
{
|
||||
public class WalletSendVaultModel
|
||||
public class WalletSendVaultModel : IHasBackAndReturnUrl
|
||||
{
|
||||
public string WalletId { get; set; }
|
||||
public string WebsocketPath { get; set; }
|
||||
|
@ -2,7 +2,7 @@ using System;
|
||||
|
||||
namespace BTCPayServer.Models.WalletViewModels
|
||||
{
|
||||
public class WalletSigningOptionsModel
|
||||
public class WalletSigningOptionsModel : IHasBackAndReturnUrl
|
||||
{
|
||||
public SigningContextModel SigningContext { get; set; }
|
||||
public string BackUrl { get; set; }
|
||||
|
@ -128,7 +128,7 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||
return;
|
||||
if (_Cts.IsCancellationRequested)
|
||||
return;
|
||||
var session = await client.CreateWebsocketNotificationSessionAsync(_Cts.Token).ConfigureAwait(false);
|
||||
var session = await client.CreateWebsocketNotificationSessionLegacyAsync(_Cts.Token).ConfigureAwait(false);
|
||||
if (!_SessionsByCryptoCode.TryAdd(network.CryptoCode, session))
|
||||
{
|
||||
await session.DisposeAsync();
|
||||
@ -291,7 +291,9 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||
result.RPCCode == RPCErrorCode.RPC_TRANSACTION_REJECTED);
|
||||
if (!accounted && payment.Accounted && tx.Confirmations != -1)
|
||||
{
|
||||
Logs.PayServer.LogInformation($"{wallet.Network.CryptoCode}: The transaction {tx.TransactionHash} has been replaced.");
|
||||
var logs = new InvoiceLogs();
|
||||
logs.Write($"The transaction {tx.TransactionHash} has been replaced.", InvoiceEventData.EventSeverity.Warning);
|
||||
await _InvoiceRepository.AddInvoiceLogs(invoice.Id, logs);
|
||||
}
|
||||
if (paymentData.PayjoinInformation is PayjoinInformation pj)
|
||||
{
|
||||
|
@ -29,14 +29,27 @@ namespace BTCPayServer.Services.Mails
|
||||
var store = await StoreRepository.FindStore(StoreId);
|
||||
if (store is null)
|
||||
return null;
|
||||
var emailSettings = GetCustomSettings(store);
|
||||
if (emailSettings is not null)
|
||||
return emailSettings;
|
||||
if (FallbackSender is not null)
|
||||
return await FallbackSender.GetEmailSettings();
|
||||
return null;
|
||||
}
|
||||
public async Task<EmailSettings?> GetCustomSettings()
|
||||
{
|
||||
var store = await StoreRepository.FindStore(StoreId);
|
||||
if (store is null)
|
||||
return null;
|
||||
return GetCustomSettings(store);
|
||||
}
|
||||
EmailSettings? GetCustomSettings(StoreData store)
|
||||
{
|
||||
var emailSettings = store.GetStoreBlob().EmailSettings;
|
||||
if (emailSettings?.IsComplete() is true)
|
||||
{
|
||||
return emailSettings;
|
||||
}
|
||||
|
||||
if (FallbackSender is not null)
|
||||
return await FallbackSender.GetEmailSettings();
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -26,12 +26,13 @@ namespace BTCPayServer.Services
|
||||
public async Task<T?> GetSettingAsync<T>(string? name = null) where T : class
|
||||
{
|
||||
name ??= typeof(T).FullName ?? string.Empty;
|
||||
return await _memoryCache.GetOrCreateAsync(GetCacheKey(name), async entry =>
|
||||
var data = await _memoryCache.GetOrCreateAsync(GetCacheKey(name), async entry =>
|
||||
{
|
||||
await using var ctx = _ContextFactory.CreateContext();
|
||||
var data = await ctx.Settings.Where(s => s.Id == name).FirstOrDefaultAsync();
|
||||
return data == null ? default : Deserialize<T>(data.Value);
|
||||
return data?.Value;
|
||||
});
|
||||
return data is string ? Deserialize<T>(data) : null;
|
||||
}
|
||||
public async Task UpdateSetting<T>(T obj, string? name = null) where T : class
|
||||
{
|
||||
@ -49,7 +50,7 @@ namespace BTCPayServer.Services
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
_memoryCache.Set(GetCacheKey(name), obj);
|
||||
_memoryCache.Remove(GetCacheKey(name));
|
||||
_EventAggregator.Publish(new SettingsChanged<T>()
|
||||
{
|
||||
Settings = obj,
|
||||
|
@ -613,7 +613,7 @@ retry:
|
||||
return data.ToDictionary(pair => pair.Key, pair => Deserialize<T>(pair.Value.Value));
|
||||
}
|
||||
|
||||
public async Task UpdateSetting<T>(string storeId, string name, T obj) where T : class
|
||||
public async Task UpdateSetting<T>(string storeId, string name, T? obj) where T : class
|
||||
{
|
||||
await using var ctx = _ContextFactory.CreateContext();
|
||||
StoreSettingData? settings = null;
|
||||
|
@ -269,7 +269,7 @@ namespace BTCPayServer.Services.Wallets
|
||||
SeenAt = row.seen_at,
|
||||
TransactionId = uint256.Parse(row.tx_id),
|
||||
Confirmations = row.confs,
|
||||
BlockHash = string.IsNullOrEmpty(row.asset_id) ? null : uint256.Parse(row.blk_id)
|
||||
BlockHash = string.IsNullOrEmpty(row.blk_id) ? null : uint256.Parse(row.blk_id)
|
||||
});
|
||||
}
|
||||
return lines;
|
||||
|
@ -49,7 +49,7 @@
|
||||
<label asp-for="Settings.Password" class="form-label" text-translate="true">Password</label>
|
||||
@if (!Model.PasswordSet)
|
||||
{
|
||||
<input asp-for="Settings.Password" type="password" class="form-control"/>
|
||||
<input asp-for="Settings.Password" type="password" value="@Model.Settings.Password" class="form-control" />
|
||||
<span asp-validation-for="Settings.Password" class="text-danger"></span>
|
||||
}
|
||||
else
|
||||
|
@ -1,6 +1,7 @@
|
||||
@model BTCPayServer.Abstractions.Form.Field
|
||||
@{
|
||||
var isInvalid = ViewContext.ModelState[Model.Name]?.ValidationState is Microsoft.AspNetCore.Mvc.ModelBinding.ModelValidationState.Invalid;
|
||||
var isCheckbox = Model.Type == "checkbox";
|
||||
var errors = isInvalid ? ViewContext.ModelState[Model.Name].Errors : null;
|
||||
}
|
||||
@if (Model.Type == "hidden")
|
||||
@ -8,21 +9,43 @@
|
||||
<input id="@Model.Name" type="@Model.Type" name="@Model.Name" value="@Model.Value" />
|
||||
return;
|
||||
}
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="@Model.Name"@(Model.Required ? " data-required" : "")>
|
||||
@Safe.Raw(Model.Label)
|
||||
</label>
|
||||
<input id="@Model.Name" type="@Model.Type" class="form-control @(errors is null ? "" : "is-invalid")"
|
||||
name="@Model.Name" value="@Model.Value" data-val="true" readonly="@Model.Constant"
|
||||
@if (!string.IsNullOrEmpty(Model.HelpText))
|
||||
{
|
||||
@Safe.Raw($" aria-describedby=\"HelpText-{Model.Name}\"")
|
||||
}
|
||||
@if (Model.Required)
|
||||
{
|
||||
@Safe.Raw($" data-val-required=\"{Model.Label} is required.\" required")
|
||||
}
|
||||
/>
|
||||
<div class="form-@(isCheckbox ? "check" : "group")">
|
||||
@if (isCheckbox)
|
||||
{
|
||||
<input id="@Model.Name" type="@Model.Type" class="form-check-input @(errors is null ? "" : "is-invalid")"
|
||||
name="@Model.Name" value="true" data-val="true" readonly="@Model.Constant"
|
||||
@if (Model.Value == "true")
|
||||
{
|
||||
@Safe.Raw(" checked")
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(Model.HelpText))
|
||||
{
|
||||
@Safe.Raw($" aria-describedby=\"HelpText-{Model.Name}\"")
|
||||
}
|
||||
@if (Model.Required)
|
||||
{
|
||||
@Safe.Raw($" data-val-required=\"{Model.Label} is required.\" required")
|
||||
} />
|
||||
<label class="form-check-label" for="@Model.Name"@(Model.Required ? " data-required" : "")>
|
||||
@Safe.Raw(Model.Label)
|
||||
</label>
|
||||
}
|
||||
else
|
||||
{
|
||||
<label class="form-label" for="@Model.Name"@(Model.Required ? " data-required" : "")>
|
||||
@Safe.Raw(Model.Label)
|
||||
</label>
|
||||
<input id="@Model.Name" type="@Model.Type" class="form-control @(errors is null ? "" : "is-invalid")"
|
||||
name="@Model.Name" value="@Model.Value" data-val="true" readonly="@Model.Constant"
|
||||
@if (!string.IsNullOrEmpty(Model.HelpText))
|
||||
{
|
||||
@Safe.Raw($" aria-describedby=\"HelpText-{Model.Name}\"")
|
||||
}
|
||||
@if (Model.Required)
|
||||
{
|
||||
@Safe.Raw($" data-val-required=\"{Model.Label} is required.\" required")
|
||||
} />
|
||||
}
|
||||
<span class="text-danger" data-valmsg-for="@Model.Name" data-valmsg-replace="true">@(isInvalid && errors.Any() ? errors.First().ErrorMessage : string.Empty)</span>
|
||||
@if (!string.IsNullOrEmpty(Model.HelpText))
|
||||
{
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
<script>
|
||||
if (window.localStorage.getItem('btcpay-hide-sensitive-info') === 'true') { document.documentElement.setAttribute('data-hide-sensitive-info', 'true')}
|
||||
if (window.location !== window.parent.location) { document.documentElement.setAttribute('data-within-iframe', 'true')}
|
||||
if (window.location !== window.parent.location) { window.addEventListener('message', function(event) { try { const data = JSON.parse(event.data); if (data.context) { document.documentElement.setAttribute('data-context', data.context) } } catch {} }, false); }
|
||||
</script>
|
||||
@if (Theme.CustomTheme && Theme.CustomThemeCssUrl is not null)
|
||||
{ // new customization uses theme file id provided by upload
|
||||
|
@ -78,13 +78,13 @@
|
||||
</table>
|
||||
@if (!string.IsNullOrEmpty(providedComment))
|
||||
{
|
||||
<div>
|
||||
<div class="p-2">
|
||||
<b>LNURL Comment</b>: @providedComment
|
||||
</div>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(consumedLightningAddress))
|
||||
{
|
||||
<div>
|
||||
<div class="p-2">
|
||||
<b>Lightning address used</b>: @consumedLightningAddress
|
||||
</div>
|
||||
}
|
||||
|
15
BTCPayServer/Views/Shared/_BackAndReturn.cshtml
Normal file
15
BTCPayServer/Views/Shared/_BackAndReturn.cshtml
Normal file
@ -0,0 +1,15 @@
|
||||
@model BTCPayServer.Models.IHasBackAndReturnUrl
|
||||
@{
|
||||
(var backUrl, var cancelUrl) = this.Model.NormalizeBackAndReturnUrl();
|
||||
cancelUrl ??= Context.Request.Query["returnUrl"].ToString();
|
||||
}
|
||||
|
||||
@if (backUrl != null)
|
||||
{
|
||||
<a href="@Url.EnsureLocal(backUrl, Context.Request)" id="GoBack">
|
||||
<vc:icon symbol="back" />
|
||||
</a>
|
||||
}
|
||||
<a href="@Url.EnsureLocal(cancelUrl, Context.Request)" id="CancelWizard" class="cancel">
|
||||
<vc:icon symbol="cross" />
|
||||
</a>
|
@ -149,7 +149,12 @@
|
||||
</div>
|
||||
</template>
|
||||
<template id="field-type-input">
|
||||
<div class="form-group mb-0">
|
||||
<div class="form-check mb-0" v-if="type === 'checkbox'">
|
||||
<input class="form-check-input" :id="name" :name="name" :type="type" v-model="value" />
|
||||
<label class="form-check-label" :for="name" :data-required="required" v-sanitize="label"></label>
|
||||
<div v-if="helpText" :id="`HelpText-{name}`" class="form-text" v-sanitize="helpText"></div>
|
||||
</div>
|
||||
<div class="form-groupcheck mb-0" v-else>
|
||||
<label class="form-label" :for="name" :data-required="required" v-sanitize="label"></label>
|
||||
<input class="form-control" :id="name" :name="name" :type="type" v-model="value" />
|
||||
<div v-if="helpText" :id="`HelpText-{name}`" class="form-text" v-sanitize="helpText"></div>
|
||||
@ -208,7 +213,7 @@
|
||||
</h2>
|
||||
</nav>
|
||||
<div>
|
||||
<button id="page-primary" type="submit" class="btn btn-primary order-sm-1">Save</button>
|
||||
<button id="page-primary" type="submit" class="btn btn-primary order-sm-1">Save</button>
|
||||
@if (!isNew)
|
||||
{
|
||||
<a class="btn btn-secondary" asp-action="ViewPublicForm" asp-route-formId="@formId" id="ViewForm" text-translate="true">View</a>
|
||||
|
@ -46,7 +46,7 @@
|
||||
<form asp-action="EditLightningAddress" method="post">
|
||||
@{
|
||||
var showAddForm = !ViewContext.ViewData.ModelState.IsValid || !string.IsNullOrEmpty(Model.Add?.Username) || Model.Add?.Max != null || Model.Add?.Min != null || !string.IsNullOrEmpty(Model.Add?.CurrencyCode);
|
||||
var showAdvancedOptions = !string.IsNullOrEmpty(Model.Add?.CurrencyCode) || Model.Add?.Min != null || Model.Add?.Max != null;
|
||||
var showAdvancedOptions = !string.IsNullOrEmpty(Model.Add?.CurrencyCode) || !string.IsNullOrEmpty(Model.Add?.InvoiceMetadata) || Model.Add?.Min != null || Model.Add?.Max != null;
|
||||
}
|
||||
|
||||
<div class="collapse @(showAddForm ? "show": "")" id="AddAddress">
|
||||
|
@ -14,7 +14,6 @@
|
||||
var store = ViewContext.HttpContext.GetStoreData();
|
||||
}
|
||||
|
||||
<h2 class="mb-2 mb-lg-3">@ViewData["Title"]</h2>
|
||||
<partial name="_StatusMessage" />
|
||||
|
||||
@if (Model.IsSetUp)
|
||||
@ -53,7 +52,7 @@
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<div id="Dashboard" class="mt-4">
|
||||
<div id="Dashboard">
|
||||
<vc:ui-extension-point location="dashboard" model="@Model" />
|
||||
@if (Model.WalletEnabled)
|
||||
{
|
||||
|
@ -2,34 +2,33 @@
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@model BTCPayServer.Models.EmailsViewModel
|
||||
@{
|
||||
var storeId = Context.GetStoreData().Id;
|
||||
var hasCustomSettings = (Model.IsSetup() && !Model.UsesFallback()) || ViewBag.UseCustomSMTP ?? false;
|
||||
ViewData.SetActivePage(StoreNavPages.Emails, StringLocalizer["Email Rules"], storeId);
|
||||
var storeId = Context.GetStoreData().Id;
|
||||
ViewData.SetActivePage(StoreNavPages.Emails, StringLocalizer["Email Rules"], storeId);
|
||||
}
|
||||
|
||||
<form method="post" autocomplete="off" permissioned="@Policies.CanModifyStoreSettings">
|
||||
<div class="sticky-header">
|
||||
<h2 text-translate="true">Email Server</h2>
|
||||
<div class="sticky-header">
|
||||
<h2 text-translate="true">Email Server</h2>
|
||||
<button id="page-primary" type="submit" class="btn btn-primary" name="command" value="Save">Save</button>
|
||||
</div>
|
||||
<partial name="_StatusMessage" />
|
||||
@if (Model.IsFallbackSetup())
|
||||
{
|
||||
<label class="d-flex align-items-center mb-4">
|
||||
<input type="checkbox" id="UseCustomSMTP" name="UseCustomSMTP" value="true" checked="@hasCustomSettings" class="btcpay-toggle me-3" data-bs-toggle="collapse" data-bs-target="#SmtpSettings" aria-expanded="@hasCustomSettings" aria-controls="SmtpSettings" />
|
||||
<div>
|
||||
<span text-translate="true">Use custom SMTP settings for this store</span>
|
||||
<div class="form-text" text-translate="true">Otherwise, the server's SMTP settings will be used to send emails.</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<partial name="_StatusMessage" />
|
||||
@if (Model.IsFallbackSetup)
|
||||
{
|
||||
<label class="d-flex align-items-center mb-4">
|
||||
<input type="checkbox" asp-for="IsCustomSMTP" class="btcpay-toggle me-3" data-bs-toggle="collapse" data-bs-target="#SmtpSettings" aria-expanded="@Model.IsCustomSMTP" aria-controls="SmtpSettings" />
|
||||
<div>
|
||||
<span text-translate="true">Use custom SMTP settings for this store</span>
|
||||
<div class="form-text" text-translate="true">Otherwise, the server's SMTP settings will be used to send emails.</div>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<div class="collapse @(hasCustomSettings ? "show" : "")" id="SmtpSettings">
|
||||
<partial name="EmailsBody" model="Model" />
|
||||
<div class="collapse @(Model.IsCustomSMTP ? "show" : "")" id="SmtpSettings">
|
||||
<partial name="EmailsBody" model="Model" />
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<input type="hidden" name="UseCustomSMTP" value="true" />
|
||||
<input type="hidden" id="IsCustomSMTPHidden" asp-for="IsCustomSMTP" />
|
||||
<partial name="EmailsBody" model="Model" />
|
||||
}
|
||||
|
||||
|
@ -1,23 +1,14 @@
|
||||
@using BTCPayServer.Controllers
|
||||
@model SignWithSeedViewModel
|
||||
@{
|
||||
var walletId = Context.GetRouteValue("walletId").ToString();
|
||||
var cancelUrl = Model.ReturnUrl ?? Url.Action(nameof(UIWalletsController.WalletTransactions), new { walletId });
|
||||
var backUrl = Model.BackUrl != null ? $"{Model.BackUrl}?returnUrl={Model.ReturnUrl}" : null;
|
||||
var walletId = Context.GetRouteValue("walletId").ToString();
|
||||
Model.ReturnUrl ??= Url.WalletTransactions(walletId);
|
||||
Layout = "_LayoutWizard";
|
||||
ViewData.SetActivePage(WalletsNavPages.Send, StringLocalizer["Sign PSBT"], walletId);
|
||||
}
|
||||
|
||||
@section Navbar {
|
||||
@if (backUrl != null)
|
||||
{
|
||||
<a href="@Url.EnsureLocal(backUrl, Context.Request)" id="GoBack">
|
||||
<vc:icon symbol="back" />
|
||||
</a>
|
||||
}
|
||||
<a href="@Url.EnsureLocal(cancelUrl, Context.Request)" id="CancelWizard" class="cancel">
|
||||
<vc:icon symbol="cross" />
|
||||
</a>
|
||||
<partial name="_BackAndReturn" model="Model" />
|
||||
}
|
||||
|
||||
<header class="text-center">
|
||||
|
@ -3,23 +3,14 @@
|
||||
@model WalletPSBTViewModel
|
||||
@{
|
||||
var walletId = Context.GetRouteValue("walletId").ToString();
|
||||
var cancelUrl = Model.ReturnUrl ?? Url.Action(nameof(UIWalletsController.WalletTransactions), new { walletId });
|
||||
var backUrl = Model.BackUrl != null ? $"{Model.BackUrl}?returnUrl={Model.ReturnUrl}" : null;
|
||||
Model.ReturnUrl ??= Url.WalletTransactions(walletId);
|
||||
Layout = "_LayoutWizard";
|
||||
ViewData.SetActivePage(WalletsNavPages.PSBT, StringLocalizer["Decode PSBT"], walletId);
|
||||
Csp.UnsafeEval();
|
||||
}
|
||||
|
||||
@section Navbar {
|
||||
@if (backUrl != null)
|
||||
{
|
||||
<a href="@Url.EnsureLocal(backUrl, Context.Request)" id="GoBack">
|
||||
<vc:icon symbol="back" />
|
||||
</a>
|
||||
}
|
||||
<a href="@Url.EnsureLocal(cancelUrl, Context.Request)" id="CancelWizard" class="cancel">
|
||||
<vc:icon symbol="cross" />
|
||||
</a>
|
||||
<partial name="_BackAndReturn" model="Model" />
|
||||
}
|
||||
|
||||
@section PageHeadContent {
|
||||
|
@ -2,22 +2,13 @@
|
||||
@model WalletPSBTCombineViewModel
|
||||
@{
|
||||
var walletId = Context.GetRouteValue("walletId").ToString();
|
||||
var cancelUrl = Model.ReturnUrl ?? Url.Action(nameof(UIWalletsController.WalletTransactions), new { walletId });
|
||||
var backUrl = Model.BackUrl != null ? $"{Model.BackUrl}?returnUrl={Model.ReturnUrl}" : null;
|
||||
Model.ReturnUrl ??= Url.WalletTransactions(walletId);
|
||||
Layout = "_LayoutWizard";
|
||||
ViewData.SetActivePage(WalletsNavPages.PSBT, StringLocalizer["Combine PSBT"], walletId);
|
||||
}
|
||||
|
||||
@section Navbar {
|
||||
@if (backUrl != null)
|
||||
{
|
||||
<a href="@Url.EnsureLocal(backUrl, Context.Request)" id="GoBack">
|
||||
<vc:icon symbol="back" />
|
||||
</a>
|
||||
}
|
||||
<a href="@Url.EnsureLocal(cancelUrl, Context.Request)" id="CancelWizard" class="cancel">
|
||||
<vc:icon symbol="cross" />
|
||||
</a>
|
||||
<partial name="_BackAndReturn" model="Model" />
|
||||
}
|
||||
|
||||
<header class="text-center">
|
||||
|
@ -2,10 +2,9 @@
|
||||
@inject BTCPayServer.Security.ContentSecurityPolicies Csp
|
||||
@model WalletPSBTViewModel
|
||||
@{
|
||||
var walletId = Context.GetRouteValue("walletId").ToString();
|
||||
var cancelUrl = Model.ReturnUrl ?? Url.Action(nameof(UIWalletsController.WalletTransactions), new {walletId});
|
||||
var backUrl = Model.BackUrl != null ? $"{Model.BackUrl}?returnUrl={Model.ReturnUrl}" : null;
|
||||
var isReady = !Model.HasErrors;
|
||||
var walletId = Context.GetRouteValue("walletId").ToString();
|
||||
Model.ReturnUrl ??= Url.WalletTransactions(walletId);
|
||||
var isReady = !Model.HasErrors;
|
||||
var isSignable = !isReady;
|
||||
var needsExport = !isSignable && !isReady;
|
||||
Layout = "_LayoutWizard";
|
||||
@ -78,15 +77,7 @@
|
||||
}
|
||||
|
||||
@section Navbar {
|
||||
@if (backUrl != null)
|
||||
{
|
||||
<a href="@Url.EnsureLocal(backUrl, Context.Request)" id="GoBack">
|
||||
<vc:icon symbol="back" />
|
||||
</a>
|
||||
}
|
||||
<a href="@Url.EnsureLocal(cancelUrl, Context.Request)" id="CancelWizard" class="cancel">
|
||||
<vc:icon symbol="cross" />
|
||||
</a>
|
||||
<partial name="_BackAndReturn" model="Model" />
|
||||
}
|
||||
|
||||
<header class="text-center mb-3">
|
||||
@ -212,6 +203,7 @@ else
|
||||
<form method="post" asp-action="WalletPSBT" asp-route-walletId="@walletId" enctype="multipart/form-data" class="mb-2">
|
||||
<input type="hidden" asp-for="ReturnUrl" />
|
||||
<input type="hidden" asp-for="BackUrl" />
|
||||
<partial name="SigningContext" for="SigningContext" />
|
||||
<div class="form-group">
|
||||
<label for="ImportedPSBT" class="form-label" text-translate="true">PSBT content</label>
|
||||
<textarea id="ImportedPSBT" name="PSBT" class="form-control" rows="5"></textarea>
|
||||
|
@ -7,8 +7,7 @@
|
||||
@model WalletSendModel
|
||||
@{
|
||||
var walletId = Context.GetRouteValue("walletId").ToString();
|
||||
var cancelUrl = Model.ReturnUrl ?? Url.Action(nameof(UIWalletsController.WalletTransactions), new { walletId });
|
||||
var backUrl = Model.BackUrl != null ? $"{Model.BackUrl}?returnUrl={Model.ReturnUrl}" : null;
|
||||
Model.ReturnUrl ??= Url.WalletTransactions(walletId);
|
||||
Layout = "_LayoutWizard";
|
||||
ViewData.SetActivePage(WalletsNavPages.Send, StringLocalizer["Send {0}", Model.CryptoCode], walletId);
|
||||
Csp.Add("worker-src", "blob:");
|
||||
@ -16,15 +15,7 @@
|
||||
}
|
||||
|
||||
@section Navbar {
|
||||
@if (backUrl != null)
|
||||
{
|
||||
<a href="@Url.EnsureLocal(backUrl, Context.Request)" id="GoBack">
|
||||
<vc:icon symbol="back" />
|
||||
</a>
|
||||
}
|
||||
<a href="@Url.EnsureLocal(cancelUrl, Context.Request)" id="CancelWizard" class="cancel">
|
||||
<vc:icon symbol="cross" />
|
||||
</a>
|
||||
<partial name="_BackAndReturn" model="Model" />
|
||||
}
|
||||
|
||||
@section PageHeadContent
|
||||
|
@ -2,22 +2,13 @@
|
||||
@model WalletSendVaultModel
|
||||
@{
|
||||
var walletId = Context.GetRouteValue("walletId").ToString();
|
||||
var cancelUrl = Model.ReturnUrl ?? Url.Action(nameof(UIWalletsController.WalletTransactions), new { walletId });
|
||||
var backUrl = Model.BackUrl != null ? $"{Model.BackUrl}?returnUrl={Model.ReturnUrl}" : null;
|
||||
Model.ReturnUrl ??= Url.WalletTransactions(walletId);
|
||||
Layout = "_LayoutWizard";
|
||||
ViewData.SetActivePage(WalletsNavPages.Send, StringLocalizer["Sign the transaction"], walletId);
|
||||
}
|
||||
|
||||
@section Navbar {
|
||||
@if (backUrl != null)
|
||||
{
|
||||
<a href="@Url.EnsureLocal(backUrl, Context.Request)" id="GoBack">
|
||||
<vc:icon symbol="back" />
|
||||
</a>
|
||||
}
|
||||
<a href="@Url.EnsureLocal(cancelUrl, Context.Request)" id="CancelWizard" class="cancel">
|
||||
<vc:icon symbol="cross" />
|
||||
</a>
|
||||
<partial name="_BackAndReturn" model="Model" />
|
||||
}
|
||||
|
||||
<header class="text-center">
|
||||
|
@ -3,22 +3,13 @@
|
||||
@inject BTCPayNetworkProvider BTCPayNetworkProvider
|
||||
@{
|
||||
var walletId = WalletId.Parse(Context.GetRouteValue("walletId").ToString());
|
||||
var cancelUrl = Model.ReturnUrl ?? Url.Action(nameof(UIWalletsController.WalletTransactions), new { walletId });
|
||||
var backUrl = Model.BackUrl != null ? $"{Model.BackUrl}?returnUrl={Model.ReturnUrl}" : null;
|
||||
Model.ReturnUrl ??= Url.WalletTransactions(walletId);
|
||||
Layout = "_LayoutWizard";
|
||||
ViewData.SetActivePage(WalletsNavPages.Send, StringLocalizer["Sign the transaction"], walletId.ToString());
|
||||
}
|
||||
|
||||
@section Navbar {
|
||||
@if (backUrl != null)
|
||||
{
|
||||
<a href="@Url.EnsureLocal(backUrl, Context.Request)" id="GoBack">
|
||||
<vc:icon symbol="back" />
|
||||
</a>
|
||||
}
|
||||
<a href="@Url.EnsureLocal(cancelUrl, Context.Request)" id="CancelWizard" class="cancel">
|
||||
<vc:icon symbol="cross" />
|
||||
</a>
|
||||
<partial name="_BackAndReturn" model="Model" />
|
||||
}
|
||||
|
||||
<header class="text-center">
|
||||
|
@ -42,10 +42,6 @@
|
||||
order: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.unconf > * {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
#LoadingIndicator {
|
||||
margin-bottom: 1.5rem;
|
||||
@ -178,38 +174,24 @@
|
||||
<div class="table-responsive-md">
|
||||
<table class="table table-hover ">
|
||||
<thead>
|
||||
<th>
|
||||
Id
|
||||
</th>
|
||||
<th>
|
||||
State
|
||||
</th>
|
||||
<th>
|
||||
Signature count
|
||||
</th>
|
||||
|
||||
<th>
|
||||
Actions
|
||||
</th>
|
||||
<th>Id</th>
|
||||
<th>State</th>
|
||||
<th>Signature count</th>
|
||||
<th>Actions</th>
|
||||
</thead>
|
||||
@foreach (var pendingTransaction in Model.PendingTransactions)
|
||||
{
|
||||
<tr>
|
||||
<td>@pendingTransaction.TransactionId</td>
|
||||
<td>@pendingTransaction.State</td>
|
||||
<td>@pendingTransaction.GetBlob().CollectedSignatures.Count</td>
|
||||
<td>
|
||||
@pendingTransaction.TransactionId
|
||||
<a asp-action="ViewPendingTransaction" asp-route-walletId="@walletId" asp-route-transactionId="@pendingTransaction.TransactionId"
|
||||
>@(pendingTransaction.State == PendingTransactionState.Signed ? "Broadcast" : "View")</a>
|
||||
-
|
||||
<a asp-action="CancelPendingTransaction"
|
||||
asp-route-walletId="@walletId" asp-route-transactionId="@pendingTransaction.TransactionId">Abort</a>
|
||||
</td>
|
||||
<td>
|
||||
@pendingTransaction.State
|
||||
</td>
|
||||
<td>
|
||||
@pendingTransaction.GetBlob().CollectedSignatures.Count
|
||||
</td>
|
||||
<td>
|
||||
<a asp-action="ViewPendingTransaction" asp-route-walletId="@walletId" asp-route-transactionId="@pendingTransaction.TransactionId">
|
||||
@(pendingTransaction.State == PendingTransactionState.Signed ? "Broadcast" : "View")
|
||||
</a>-
|
||||
<a asp-action="CancelPendingTransaction"
|
||||
asp-route-walletId="@walletId" asp-route-transactionId="@pendingTransaction.TransactionId">Abort</a>
|
||||
</tr>
|
||||
}
|
||||
</table>
|
||||
@ -232,9 +214,9 @@
|
||||
</button>
|
||||
</div>
|
||||
</th>
|
||||
<th text-translate="true" style="min-width:125px">Label</th>
|
||||
<th text-translate="true">Transaction</th>
|
||||
<th text-translate="true" class="amount-col">Amount</th>
|
||||
<th text-translate="true" style="min-width:125px">Label</th>
|
||||
<th text-translate="true">Transaction</th>
|
||||
<th text-translate="true" class="amount-col">Amount</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -251,8 +233,8 @@
|
||||
</div>
|
||||
<form id="WalletActions" method="post" asp-action="WalletActions" asp-route-walletId="@walletId" permission="@Policies.CanModifyStoreSettings" class="d-inline-flex align-items-center gap-3">
|
||||
<button id="BumpFee" name="command" type="submit" value="cpfp" class="btn btn-link">
|
||||
<vc:icon symbol="actions-send" />
|
||||
<span text-translate="true">Bump fee</span>
|
||||
<vc:icon symbol="actions-send" />
|
||||
<span text-translate="true">Bump fee</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
@ -25,7 +25,7 @@
|
||||
<vc:truncate-center text="@transaction.Id" link="@transaction.Link" classes="truncate-center-id" />
|
||||
</td>
|
||||
<td class="amount-col">
|
||||
<span data-sensitive class="text-@(transaction.Positive ? "success" : "danger")">@transaction.Balance</span>
|
||||
<span data-sensitive class="text-@(transaction.Positive ? "success" : "danger")@(transaction.IsConfirmed ? "" : " opacity-50")">@transaction.Balance</span>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<div class="d-inline-block">
|
||||
|
@ -10,7 +10,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
let config = parseConfig($config.value) || {}
|
||||
|
||||
const specialFieldTypeOptions = ['fieldset', 'textarea', 'select', 'mirror']
|
||||
const inputFieldTypeOptions = ['text', 'number', 'password', 'email', 'url', 'tel', 'date', 'hidden']
|
||||
const inputFieldTypeOptions = ['text', 'number', 'password', 'email', 'url', 'tel', 'date', 'datetime-local', 'color', 'checkbox', 'hidden']
|
||||
const fieldTypeOptions = inputFieldTypeOptions.concat(specialFieldTypeOptions)
|
||||
|
||||
const getFieldComponent = type => `field-type-${specialFieldTypeOptions.includes(type) ? type : 'input'}`
|
||||
|
@ -28,8 +28,8 @@
|
||||
}
|
||||
|
||||
/* Iframe context */
|
||||
[data-within-iframe] #StoreLink,
|
||||
[data-within-iframe] .store-footer {
|
||||
[data-context="btcpayapp"] #Checkout #StoreLink,
|
||||
[data-context="btcpayapp"] #Checkout .store-footer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,127 @@
|
||||
{
|
||||
"paths": {
|
||||
"/api/v1/server/email": {
|
||||
"get": {
|
||||
"tags": ["ServerEmail"],
|
||||
"summary": "Get server email settings",
|
||||
"description": "Retrieve the email settings configured for the server. The password field will be masked if present.",
|
||||
"operationId": "ServerEmail_GetSettings",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Server email settings",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ServerEmailSettingsData"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden - Insufficient permissions"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"API_Key": ["btcpay.server.canmodifyserversettings"],
|
||||
"Basic": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"put": {
|
||||
"tags": ["ServerEmail"],
|
||||
"summary": "Update server email settings",
|
||||
"description": "Update server's email settings.",
|
||||
"operationId": "ServerEmail_UpdateSettings",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ServerEmailSettingsData"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Email settings updated successfully"
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid request or email format",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden - Insufficient permissions"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"API_Key": ["btcpay.server.canmodifyserversettings"],
|
||||
"Basic": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"ServerEmailSettingsData": {
|
||||
"allOf": [
|
||||
{ "$ref": "#/components/schemas/EmailSettings" },
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enableStoresToUseServerEmailSettings": {
|
||||
"type": "boolean",
|
||||
"description": "Indicates if stores can use server email settings"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"EmailSettings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"from": {
|
||||
"type": "string",
|
||||
"description": "The sender email address"
|
||||
},
|
||||
"server": {
|
||||
"type": "string",
|
||||
"description": "SMTP server host"
|
||||
},
|
||||
"port": {
|
||||
"type": "integer",
|
||||
"description": "SMTP server port"
|
||||
},
|
||||
"login": {
|
||||
"type": "string",
|
||||
"description": "SMTP username"
|
||||
},
|
||||
"password": {
|
||||
"type": "string",
|
||||
"description": "SMTP password, masked in responses and retained if not updated",
|
||||
"nullable": true
|
||||
},
|
||||
"disableCertificateCheck": {
|
||||
"type": "boolean",
|
||||
"description": "Use SSL for SMTP connection"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
{
|
||||
"name": "ServerEmail",
|
||||
"description": "Server Email Settings operations"
|
||||
}
|
||||
]
|
||||
}
|
@ -11,7 +11,7 @@
|
||||
"$ref": "#/components/parameters/StoreId"
|
||||
}
|
||||
],
|
||||
"description": "View email settings of the specified store",
|
||||
"description": "Retrieve the email settings configured for specific store. The password field will be masked if present.",
|
||||
"operationId": "Stores_GetStoreEmailSettings",
|
||||
"responses": {
|
||||
"200": {
|
||||
|
Reference in New Issue
Block a user