Compare commits

...

14 Commits

Author SHA1 Message Date
ebc053aca5 Update Changelog (#5583) 2023-12-21 23:46:29 +09:00
96da7f0322 UI: Form validation summary matches alert style (#5576)
Fixes #5564.
2023-12-21 23:43:12 +09:00
8ae9e59d9d Lightning Address: Use lowercased username when resolving (#5579)
* Lightning Address: Use lowercased username when resolving

* Use static NormalizeUsername
2023-12-21 23:42:17 +09:00
c94dc87cb8 Fix: Setup a boltcard for the second time wouldn't generate new keys 2023-12-21 18:16:25 +09:00
20512a59b3 Fix API doc for boltcard related feature 2023-12-21 18:02:13 +09:00
b3f9216c54 Use PullPaymentId to derive the cardkey of Boltcard (#5575) 2023-12-21 10:29:28 +09:00
1cda0360e9 Fix test 2023-12-20 22:00:08 +09:00
7f75117bfa Fix flaky 2023-12-20 20:59:27 +09:00
5a70345499 Do not redirect to archived store after login (#5566)
Now that we have archived stores, we need to exclude them from the selection of the default store the user gets redirected to after login.
2023-12-20 19:27:02 +09:00
5114a3a2ea Lightning: Fix connection display name in LN settings (#5569)
* Lightning: Fix connection display name in LN settings

Builds on btcpayserver/BTCPayServer.Lightning#153.

* Upgrade Lightning lib
2023-12-20 19:26:24 +09:00
93ab219124 Lightning: Allow LND to be used with non-admin macaroons (#5567)
* Lightning: Allow LND to be used with non-admin macaroons

Requires btcpayserver/BTCPayServer.Lightning#152.

* Upgrade Lightning lib
2023-12-20 19:23:46 +09:00
61bf6d33b2 Handle disabled plugin in ui (#5570)
When a plugin is disabled, we should at least show the uninstall option in the plugin option. Eventually we should also detect what version was disabled and offer an update instead
2023-12-20 18:56:21 +09:00
3fc687a2d4 Fix: Payments to Top-Up could be undetected due to race condition (#5568) 2023-12-20 18:41:28 +09:00
8da04fd7e2 Better error message in Vault if hardware device isn't supported 2023-12-20 17:17:19 +09:00
82 changed files with 458 additions and 312 deletions

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@ -31,7 +32,9 @@ namespace BTCPayServer.Data
public List<InvoiceSearchData> InvoiceSearchData { get; set; }
public List<RefundData> Refunds { get; set; }
[Timestamp]
// With this, update of InvoiceData will fail if the row was modified by another process
public uint XMin { get; set; }
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
{
builder.Entity<InvoiceData>()

View File

@ -1,4 +1,4 @@
// <auto-generated />
// <auto-generated />
using System;
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore;
@ -16,25 +16,7 @@ namespace BTCPayServer.Migrations
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "6.0.9");
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
{
b.Property<string>("Address")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("CreatedTime")
.HasColumnType("TEXT");
b.Property<string>("InvoiceDataId")
.HasColumnType("TEXT");
b.HasKey("Address");
b.HasIndex("InvoiceDataId");
b.ToTable("AddressInvoices");
});
modelBuilder.HasAnnotation("ProductVersion", "8.0.0");
modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b =>
{
@ -71,6 +53,24 @@ namespace BTCPayServer.Migrations
b.ToTable("ApiKeys");
});
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
{
b.Property<string>("Address")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("CreatedTime")
.HasColumnType("TEXT");
b.Property<string>("InvoiceDataId")
.HasColumnType("TEXT");
b.HasKey("Address");
b.HasIndex("InvoiceDataId");
b.ToTable("AddressInvoices");
});
modelBuilder.Entity("BTCPayServer.Data.AppData", b =>
{
b.Property<string>("Id")
@ -89,7 +89,7 @@ namespace BTCPayServer.Migrations
.HasColumnType("TEXT");
b.Property<string>("Settings")
.HasColumnType("JSONB");
.HasColumnType("TEXT");
b.Property<string>("StoreDataId")
.HasColumnType("TEXT");
@ -305,6 +305,11 @@ namespace BTCPayServer.Migrations
b.Property<string>("StoreDataId")
.HasColumnType("TEXT");
b.Property<uint>("XMin")
.IsConcurrencyToken()
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("Created");
@ -781,31 +786,6 @@ namespace BTCPayServer.Migrations
b.ToTable("Stores");
});
modelBuilder.Entity("BTCPayServer.Data.StoredFile", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("ApplicationUserId")
.HasColumnType("TEXT");
b.Property<string>("FileName")
.HasColumnType("TEXT");
b.Property<string>("StorageFileName")
.HasColumnType("TEXT");
b.Property<DateTime>("Timestamp")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("ApplicationUserId");
b.ToTable("Files");
});
modelBuilder.Entity("BTCPayServer.Data.StoreRole", b =>
{
b.Property<string>("Id")
@ -863,6 +843,31 @@ namespace BTCPayServer.Migrations
b.ToTable("StoreWebhooks");
});
modelBuilder.Entity("BTCPayServer.Data.StoredFile", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("ApplicationUserId")
.HasColumnType("TEXT");
b.Property<string>("FileName")
.HasColumnType("TEXT");
b.Property<string>("StorageFileName")
.HasColumnType("TEXT");
b.Property<DateTime>("Timestamp")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("ApplicationUserId");
b.ToTable("Files");
});
modelBuilder.Entity("BTCPayServer.Data.U2FDevice", b =>
{
b.Property<string>("Id")
@ -1171,16 +1176,6 @@ namespace BTCPayServer.Migrations
b.ToTable("AspNetUserTokens", (string)null);
});
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("AddressInvoices")
.HasForeignKey("InvoiceDataId")
.OnDelete(DeleteBehavior.Cascade);
b.Navigation("InvoiceData");
});
modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
@ -1198,6 +1193,16 @@ namespace BTCPayServer.Migrations
b.Navigation("User");
});
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("AddressInvoices")
.HasForeignKey("InvoiceDataId")
.OnDelete(DeleteBehavior.Cascade);
b.Navigation("InvoiceData");
});
modelBuilder.Entity("BTCPayServer.Data.AppData", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
@ -1408,15 +1413,6 @@ namespace BTCPayServer.Migrations
b.Navigation("PullPaymentData");
});
modelBuilder.Entity("BTCPayServer.Data.StoredFile", b =>
{
b.HasOne("BTCPayServer.Data.ApplicationUser", "ApplicationUser")
.WithMany("StoredFiles")
.HasForeignKey("ApplicationUserId");
b.Navigation("ApplicationUser");
});
modelBuilder.Entity("BTCPayServer.Data.StoreRole", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
@ -1457,6 +1453,15 @@ namespace BTCPayServer.Migrations
b.Navigation("Webhook");
});
modelBuilder.Entity("BTCPayServer.Data.StoredFile", b =>
{
b.HasOne("BTCPayServer.Data.ApplicationUser", "ApplicationUser")
.WithMany("StoredFiles")
.HasForeignKey("ApplicationUserId");
b.Navigation("ApplicationUser");
});
modelBuilder.Entity("BTCPayServer.Data.U2FDevice", b =>
{
b.HasOne("BTCPayServer.Data.ApplicationUser", "ApplicationUser")

View File

@ -1,5 +1,4 @@
using System;
using System.ComponentModel;
using System.Globalization;
using System.IO;
using System.Linq;
@ -99,14 +98,25 @@ namespace BTCPayServer.Tests
Driver.FindElement(By.Id("FakePayment")).Click();
if (mine)
{
TestUtils.Eventually(() =>
{
Driver.WaitForElement(By.Id("CheatSuccessMessage"));
});
MineBlockOnInvoiceCheckout();
}
}
public void MineBlockOnInvoiceCheckout()
{
Driver.FindElement(By.CssSelector("#mine-block button")).Click();
retry:
try
{
Driver.FindElement(By.CssSelector("#mine-block button")).Click();
}
catch (StaleElementReferenceException)
{
goto retry;
}
}
/// <summary>

View File

@ -164,7 +164,7 @@ namespace BTCPayServer.Tests
Assert.Contains("CustomFormInputTest", s.Driver.PageSource);
s.Driver.FindElement(By.Name("buyerEmail")).SendKeys("aa@aa.com");
s.Driver.FindElement(By.CssSelector("input[type='submit']")).Click();
s.PayInvoice(true);
s.PayInvoice(true, 0.001m);
var result = await s.Server.PayTester.HttpClient.GetAsync(formurl);
Assert.Equal(HttpStatusCode.NotFound, result.StatusCode);
@ -1149,7 +1149,6 @@ namespace BTCPayServer.Tests
// Contribute
s.Driver.FindElement(By.Id("crowdfund-body-header-cta")).Click();
Thread.Sleep(1000);
s.Driver.WaitUntilAvailable(By.Name("btcpay"));
var frameElement = s.Driver.FindElement(By.Name("btcpay"));
@ -2152,7 +2151,7 @@ namespace BTCPayServer.Tests
var ppid = lnurl.AbsoluteUri.Split("/").Last();
var issuerKey = new IssuerKey(SettingsRepositoryExtensions.FixedKey());
var uid = RandomNumberGenerator.GetBytes(7);
var cardKey = issuerKey.CreateCardKey(uid, 0);
var cardKey = issuerKey.CreatePullPaymentCardKey(uid, 0, ppid);
var keys = cardKey.DeriveBoltcardKeys(issuerKey);
await db.LinkBoltcardToPullPayment(ppid, issuerKey, uid);
var piccData = new byte[] { 0xc7 }.Concat(uid).Concat(new byte[] { 1, 0, 0, 0, 0, 0, 0, 0 }).ToArray();
@ -2190,6 +2189,10 @@ namespace BTCPayServer.Tests
// Relink should bump Version
reg = await db.GetBoltcardRegistration(issuerKey, uid);
Assert.Equal((ppid, 0, 1), (reg.PullPaymentId, reg.Counter, reg.Version));
await db.LinkBoltcardToPullPayment(ppid, issuerKey, uid);
reg = await db.GetBoltcardRegistration(issuerKey, uid);
Assert.Equal((ppid, 0, 2), (reg.PullPaymentId, reg.Counter, reg.Version));
}
s.GoToStore(s.StoreId, StoreNavPages.PullPayments);

View File

@ -395,7 +395,7 @@ namespace BTCPayServer.Tests
BOLT11PaymentRequest.Parse(newBolt11, Network.RegTest).MinimumAmount.ToDecimal(LightMoneyUnit.BTC));
}, 40000);
TestLogs.LogInformation($"Paying invoice {newInvoice.Id} remaining due amount {newInvoice.BtcDue.GetValue((BTCPayNetwork) tester.DefaultNetwork)} via lightning");
TestLogs.LogInformation($"Paying invoice {newInvoice.Id} remaining due amount {newInvoice.BtcDue.GetValue((BTCPayNetwork)tester.DefaultNetwork)} via lightning");
var evt = await tester.WaitForEvent<InvoiceDataChangedEvent>(async () =>
{
await tester.SendLightningPaymentAsync(newInvoice);
@ -1301,11 +1301,8 @@ namespace BTCPayServer.Tests
var user = tester.NewAccount();
await user.GrantAccessAsync();
user.RegisterDerivationScheme("BTC");
await tester.ExplorerNode.EnsureGenerateAsync(1);
var rng = new Random();
var seed = rng.Next();
rng = new Random(seed);
TestLogs.LogInformation("Seed: " + seed);
foreach (var networkFeeMode in Enum.GetValues(typeof(NetworkFeeMode)).Cast<NetworkFeeMode>())
{
await user.SetNetworkFeeMode(networkFeeMode);
@ -1318,7 +1315,7 @@ namespace BTCPayServer.Tests
}
}
private static async Task AssertTopUpBtcPrice(ServerTester tester, TestAccount user, Money btcSent, decimal expectedPriceWithoutNetworkFee, NetworkFeeMode networkFeeMode)
private async Task AssertTopUpBtcPrice(ServerTester tester, TestAccount user, Money btcSent, decimal expectedPriceWithoutNetworkFee, NetworkFeeMode networkFeeMode)
{
var cashCow = tester.ExplorerNode;
// First we try payment with a merchant having only BTC
@ -1343,7 +1340,6 @@ namespace BTCPayServer.Tests
{
networkFee = 0.0m;
}
await cashCow.SendToAddressAsync(invoiceAddress, paid);
await TestUtils.EventuallyAsync(async () =>
{
@ -1822,7 +1818,7 @@ namespace BTCPayServer.Tests
Assert.Empty(appList2.Apps);
Assert.Equal("test", appList.Apps[0].AppName);
Assert.Equal(apps.CreatedAppId, appList.Apps[0].Id);
Assert.True(app.Role.ToPermissionSet(app.StoreId).Contains(Policies.CanModifyStoreSettings, app.StoreId));
Assert.Equal(user.StoreId, appList.Apps[0].StoreId);
Assert.IsType<NotFoundResult>(apps2.DeleteApp(appList.Apps[0].Id));
@ -2399,11 +2395,11 @@ namespace BTCPayServer.Tests
var url = lnMethod.GetExternalLightningUrl();
var kv = LightningConnectionStringHelper.ExtractValues(url, out var connType);
Assert.Equal(LightningConnectionType.Charge,connType);
Assert.Equal(LightningConnectionType.Charge, connType);
var client = Assert.IsType<ChargeClient>(tester.PayTester.GetService<LightningClientFactoryService>()
.Create(url, tester.NetworkProvider.GetNetwork<BTCPayNetwork>("BTC")));
var auth = Assert.IsType<ChargeAuthentication.UserPasswordAuthentication>(client.ChargeAuthentication);
Assert.Equal("pass", auth.NetworkCredential.Password);
Assert.Equal("usr", auth.NetworkCredential.UserName);
@ -2829,7 +2825,7 @@ namespace BTCPayServer.Tests
var app = await client.CreatePointOfSaleApp(acc.StoreId, new CreatePointOfSaleAppRequest()
{
AppName = "Static",
DefaultView = Client.Models.PosViewType.Static,
DefaultView = Client.Models.PosViewType.Static,
Template = new PointOfSaleSettings().Template
});
var resp = await posController.ViewPointOfSale(app.Id, choiceKey: "green-tea");
@ -2839,7 +2835,7 @@ namespace BTCPayServer.Tests
app = await client.CreatePointOfSaleApp(acc.StoreId, new CreatePointOfSaleAppRequest()
{
AppName = "Cart",
DefaultView = Client.Models.PosViewType.Cart,
DefaultView = Client.Models.PosViewType.Cart,
Template = new PointOfSaleSettings().Template
});
resp = await posController.ViewPointOfSale(app.Id, posData: new JObject()

View File

@ -46,11 +46,11 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="BTCPayServer.NTag424" Version="1.0.18" />
<PackageReference Include="BTCPayServer.NTag424" Version="1.0.19" />
<PackageReference Include="YamlDotNet" Version="8.0.0" />
<PackageReference Include="BIP78.Sender" Version="0.2.2" />
<PackageReference Include="BTCPayServer.Hwi" Version="2.0.2" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.5.2" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.5.3" />
<PackageReference Include="CsvHelper" Version="15.0.5" />
<PackageReference Include="Dapper" Version="2.1.24" />
<PackageReference Include="Fido2" Version="2.0.2" />

View File

@ -41,14 +41,13 @@ namespace BTCPayServer.Components.StoreSelector
.FirstOrDefault()?
.Network.CryptoCode;
var walletId = cryptoCode != null ? new WalletId(store.Id, cryptoCode) : null;
var role = store.GetStoreRoleOfUser(userId);
return new StoreSelectorOption
{
Text = store.StoreName,
Value = store.Id,
Selected = store.Id == currentStore?.Id,
WalletId = walletId,
Store = store,
Store = store
};
})
.OrderBy(s => s.Text)

View File

@ -1,6 +1,7 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.IO.IsolatedStorage;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
@ -218,7 +219,7 @@ namespace BTCPayServer.Controllers.Greenfield
var issuerKey = await _settingsRepository.GetIssuerKey(_env);
var version = await _dbContextFactory.LinkBoltcardToPullPayment(pullPaymentId, issuerKey, request.UID, request.OnExisting);
var keys = issuerKey.CreateCardKey(request.UID, version).DeriveBoltcardKeys(issuerKey);
var keys = issuerKey.CreatePullPaymentCardKey(request.UID, version, pullPaymentId).DeriveBoltcardKeys(issuerKey);
var boltcardUrl = Url.Action(nameof(UIBoltcardController.GetWithdrawRequest), "UIBoltcard");
boltcardUrl = Request.GetAbsoluteUri(boltcardUrl);

View File

@ -48,16 +48,11 @@ public class LightningAddressService
{
return await _memoryCache.GetOrCreateAsync(GetKey(username), async entry =>
{
var result = await Get(new LightningAddressQuery { Usernames = new[] { username } });
var result = await Get(new LightningAddressQuery { Usernames = new[] { NormalizeUsername(username) } });
return result.FirstOrDefault();
});
}
private string NormalizeUsername(string username)
{
return username.ToLowerInvariant();
}
public async Task<bool> Set(LightningAddressData data)
{
data.Username = NormalizeUsername(data.Username);
@ -115,8 +110,12 @@ public class LightningAddressService
await context.AddAsync(data);
}
public static string NormalizeUsername(string username)
{
return username.ToLowerInvariant();
}
private string GetKey(string username)
private static string GetKey(string username)
{
username = NormalizeUsername(username);
return $"{nameof(LightningAddressService)}_{username}";

View File

@ -61,7 +61,7 @@ public class UIBoltcardController : Controller
var registration = await ContextFactory.GetBoltcardRegistration(issuerKey, piccData, updateCounter: pr is not null);
if (registration?.PullPaymentId is null)
return BadRequest(new LNUrlStatusResponse { Status = "ERROR", Reason = "Replayed or expired query" });
var cardKey = issuerKey.CreateCardKey(piccData.Uid, registration.Version);
var cardKey = issuerKey.CreatePullPaymentCardKey(piccData.Uid, registration.Version, registration.PullPaymentId);
if (!cardKey.CheckSunMac(c, piccData))
return BadRequest(new LNUrlStatusResponse { Status = "ERROR", Reason = "Replayed or expired query" });
LNURLController.ControllerContext.HttpContext = HttpContext;

View File

@ -84,8 +84,9 @@ namespace BTCPayServer.Controllers
}
var stores = await _storeRepository.GetStoresByUserId(userId);
return stores.Any()
? RedirectToStore(userId, stores.First())
var activeStore = stores.FirstOrDefault(s => !s.Archived);
return activeStore != null
? RedirectToStore(userId, activeStore)
: RedirectToAction(nameof(UIUserStoresController.CreateStore), "UIUserStores");
}

View File

@ -373,7 +373,7 @@ namespace BTCPayServer
if (string.IsNullOrEmpty(username))
return NotFound("Unknown username");
LNURLPayRequest lnurlRequest = null;
LNURLPayRequest lnurlRequest;
// Check core and fall back to lookup Lightning Address via plugins
var lightningAddressSettings = await _lightningAddressService.ResolveByAddress(username);

View File

@ -111,7 +111,7 @@ next:
try
{
var version = await _dbContextFactory.LinkBoltcardToPullPayment(pullPaymentId, issuerKey, uid);
var cardKey = issuerKey.CreateCardKey(uid, version);
var cardKey = issuerKey.CreatePullPaymentCardKey(uid, version, pullPaymentId);
await ntag.SetupBoltcard(boltcardUrl, BoltcardKeys.Default, cardKey.DeriveBoltcardKeys(issuerKey));
}
catch
@ -135,7 +135,7 @@ next:
}
else if (cardOrigin is CardOrigin.ThisIssuer thisIssuer)
{
var cardKey = issuerKey.CreateCardKey(thisIssuer.Registration.UId, thisIssuer.Registration.Version);
var cardKey = issuerKey.CreatePullPaymentCardKey(thisIssuer.Registration.UId, thisIssuer.Registration.Version, pullPaymentId);
await ntag.ResetCard(issuerKey, cardKey);
await _dbContextFactory.SetBoltcardResetState(issuerKey, thisIssuer.Registration.UId);
await vaultClient.Show(VaultMessageType.Ok, "Card reset succeed", cts.Token);

View File

@ -310,10 +310,11 @@ askdevice:
await websocketHelper.Send("{ \"error\": \"no-device\"}", cancellationToken);
continue;
}
device = new HwiDeviceClient(hwi, deviceEntry.DeviceSelector, deviceEntry.Model, deviceEntry.Fingerprint);
var model = deviceEntry.Model ?? "Unsupported hardware wallet, try to update BTCPay Server Vault";
device = new HwiDeviceClient(hwi, deviceEntry.DeviceSelector, model, deviceEntry.Fingerprint);
fingerprint = device.Fingerprint;
JObject json = new JObject();
json.Add("model", device.Model);
json.Add("model", model);
json.Add("fingerprint", device.Fingerprint?.ToString());
await websocketHelper.Send(json.ToString(), cancellationToken);
break;

View File

@ -21,7 +21,7 @@ public static class BoltcardDataExtensions
string onConflict = onExisting switch
{
OnExistingBehavior.KeepVersion => "UPDATE SET ppid=excluded.ppid, version=boltcards.version",
OnExistingBehavior.UpdateVersion => "UPDATE SET ppid=excluded.ppid, version=excluded.version+1",
OnExistingBehavior.UpdateVersion => "UPDATE SET ppid=excluded.ppid, version=boltcards.version+1",
_ => throw new NotSupportedException()
};
return await conn.QueryFirstOrDefaultAsync<int>(

View File

@ -50,6 +50,7 @@ namespace BTCPayServer.Data
public static StoreBlob GetStoreBlob(this StoreData storeData)
{
ArgumentNullException.ThrowIfNull(storeData);
var result = storeData.StoreBlob == null ? new StoreBlob() : new Serializer(null).ToObject<StoreBlob>(storeData.StoreBlob);
if (result.PreferredExchange == null)
result.PreferredExchange = result.GetRecommendedExchange();

View File

@ -19,6 +19,7 @@ using BTCPayServer.Lightning;
using BTCPayServer.Logging;
using BTCPayServer.Models;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.NTag424;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Security;
@ -41,6 +42,11 @@ namespace BTCPayServer
{
public static class Extensions
{
public static CardKey CreatePullPaymentCardKey(this IssuerKey issuerKey, byte[] uid, int version, string pullPaymentId)
{
var data = Encoding.UTF8.GetBytes(pullPaymentId);
return issuerKey.CreateCardKey(uid, version, data);
}
public static DateTimeOffset TruncateMilliSeconds(this DateTimeOffset dt) => new (dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, dt.Second, 0, dt.Offset);
public static decimal? GetDue(this InvoiceCryptoInfo invoiceCryptoInfo)
{
@ -94,20 +100,21 @@ namespace BTCPayServer
public static string GetDisplayName(this ILightningClient client)
{
LightningConnectionStringHelper.ExtractValues(client.ToString(), out var type);
var field = typeof(LightningConnectionType).GetField(type, BindingFlags.Public | BindingFlags.Static);
var lncType = typeof(LightningConnectionType);
var fields = lncType.GetFields(BindingFlags.Public | BindingFlags.Static);
var field = fields.FirstOrDefault(f => f.GetValue(lncType)?.ToString() == type);
if (field == null) return type;
DisplayAttribute attr = field.GetCustomAttribute<DisplayAttribute>();
return attr?.Name ?? type;
}
public static bool IsSafe(this ILightningClient connectionString)
public static bool IsSafe(this ILightningClient client)
{
var kv = LightningConnectionStringHelper.ExtractValues(connectionString.ToString(), out var type);
if (kv.TryGetValue("cookiefilepath", out var cookieFilePath) ||
kv.TryGetValue("macaroondirectorypath", out var macaroonDirectoryPath) ||
kv.TryGetValue("macaroonfilepath", out var macaroonFilePath) )
var kv = LightningConnectionStringHelper.ExtractValues(client.ToString(), out var type);
if (kv.TryGetValue("cookiefilepath", out _) ||
kv.TryGetValue("macaroondirectorypath", out _) ||
kv.TryGetValue("macaroonfilepath", out _) )
return false;
if (!kv.TryGetValue("server", out var server))
@ -117,7 +124,7 @@ namespace BTCPayServer
var uri = new Uri(server, UriKind.Absolute);
if (uri.Scheme.Equals("unix", StringComparison.OrdinalIgnoreCase))
return false;
if (!Utils.TryParseEndpoint(uri.DnsSafeHost, 80, out var endpoint))
if (!Utils.TryParseEndpoint(uri.DnsSafeHost, 80, out _))
return false;
return !IsLocalNetwork(uri.DnsSafeHost);
}

View File

@ -38,11 +38,10 @@ namespace BTCPayServer.HostedServices
public bool Dirty => _dirty;
bool _isBlobUpdated;
public bool IsBlobUpdated => _isBlobUpdated;
public void BlobUpdated()
public bool IsPriceUpdated { get; private set; }
public void PriceUpdated()
{
_isBlobUpdated = true;
IsPriceUpdated = true;
}
}
@ -104,7 +103,7 @@ namespace BTCPayServer.HostedServices
var payment = invoice.GetPayments(true).First();
invoice.Price = payment.InvoicePaidAmount.Net;
invoice.UpdateTotals();
context.BlobUpdated();
context.PriceUpdated();
}
else
{
@ -291,9 +290,9 @@ namespace BTCPayServer.HostedServices
await _invoiceRepository.UpdateInvoiceStatus(invoice.Id, invoice.GetInvoiceState());
updateContext.Events.Insert(0, new InvoiceDataChangedEvent(invoice));
}
if (updateContext.IsBlobUpdated)
if (updateContext.IsPriceUpdated)
{
await _invoiceRepository.UpdateInvoicePrice(invoice.Id, invoice);
await _invoiceRepository.UpdateInvoicePrice(invoice.Id, invoice.Price);
}
foreach (var evt in updateContext.Events)

View File

@ -61,7 +61,7 @@ namespace BTCPayServer.Payments.Lightning
if (preparePaymentObject is null)
{
return new LightningLikePaymentMethodDetails()
return new LightningLikePaymentMethodDetails
{
Activated = false
};
@ -144,6 +144,12 @@ namespace BTCPayServer.Payments.Lightning
}
catch (NotSupportedException)
{
// LNDhub, LNbits and others might not support this call, yet we can create invoices.
return new NodeInfo[] {};
}
catch (UnauthorizedAccessException)
{
// LND might return this with restricted macaroon, support this nevertheless..
return new NodeInfo[] {};
}
catch (Exception ex)
@ -237,7 +243,7 @@ namespace BTCPayServer.Payments.Lightning
public override CheckoutUIPaymentMethodSettings GetCheckoutUISettings()
{
return new CheckoutUIPaymentMethodSettings()
return new CheckoutUIPaymentMethodSettings
{
ExtensionPartial = "Lightning/LightningLikeMethodCheckout",
CheckoutBodyVueComponentName = "LightningLikeMethodCheckout",

View File

@ -30,11 +30,11 @@ namespace BTCPayServer.Payments.Lightning
#pragma warning restore CS0618 // Type or member is obsolete
}
public void SetLightningUrl(ILightningClient connectionString)
public void SetLightningUrl(ILightningClient client)
{
ArgumentNullException.ThrowIfNull(connectionString);
ArgumentNullException.ThrowIfNull(client);
#pragma warning disable CS0618 // Type or member is obsolete
LightningConnectionString = connectionString.ToString();
LightningConnectionString = client.ToString();
#pragma warning restore CS0618 // Type or member is obsolete
}

View File

@ -0,0 +1,9 @@
using LNURL;
namespace BTCPayServer.Plugins;
public class LightningAddressResolver(string username)
{
public string Username { get; set; } = LightningAddressService.NormalizeUsername(username);
public LNURLPayRequest LNURLPayRequest { get; set; }
}

View File

@ -1,14 +0,0 @@
using LNURL;
namespace BTCPayServer.Plugins;
public class LightningAddressResolver
{
public string Username { get; set; }
public LNURLPayRequest LNURLPayRequest { get; set; }
public LightningAddressResolver(string username)
{
Username = username;
}
}

View File

@ -79,13 +79,13 @@ namespace BTCPayServer.Services.Apps
public async Task<object?> GetInfo(string appId)
{
var appData = await GetApp(appId, null);
var appData = await GetApp(appId, null, includeStore: true);
if (appData is null)
return null;
var appType = GetAppType(appData.AppType);
if (appType is null)
return null;
return appType.GetInfo(appData);
return await appType.GetInfo(appData);
}
public async Task<IEnumerable<ItemStats>> GetItemStats(AppData appData)

View File

@ -11,6 +11,7 @@ using BTCPayServer.Events;
using BTCPayServer.Logging;
using BTCPayServer.Models.InvoicingModels;
using BTCPayServer.Payments;
using Dapper;
using Microsoft.EntityFrameworkCore;
using NBitcoin;
using Newtonsoft.Json;
@ -130,32 +131,50 @@ namespace BTCPayServer.Services.Invoices
public async Task UpdateInvoice(string invoiceId, UpdateCustomerModel data)
{
using var ctx = _applicationDbContextFactory.CreateContext();
var invoiceData = await ctx.Invoices.FindAsync(invoiceId).ConfigureAwait(false);
if (invoiceData == null)
return;
if (invoiceData.CustomerEmail == null && data.Email != null)
retry:
using (var ctx = _applicationDbContextFactory.CreateContext())
{
invoiceData.CustomerEmail = data.Email;
AddToTextSearch(ctx, invoiceData, invoiceData.CustomerEmail);
var invoiceData = await ctx.Invoices.FindAsync(invoiceId);
if (invoiceData == null)
return;
if (invoiceData.CustomerEmail == null && data.Email != null)
{
invoiceData.CustomerEmail = data.Email;
AddToTextSearch(ctx, invoiceData, invoiceData.CustomerEmail);
}
try
{
await ctx.SaveChangesAsync().ConfigureAwait(false);
}
catch (DbUpdateConcurrencyException)
{
goto retry;
}
}
await ctx.SaveChangesAsync().ConfigureAwait(false);
}
public async Task UpdateInvoiceExpiry(string invoiceId, TimeSpan seconds)
{
await using var ctx = _applicationDbContextFactory.CreateContext();
var invoiceData = await ctx.Invoices.FindAsync(invoiceId);
var invoice = invoiceData.GetBlob(_btcPayNetworkProvider);
var expiry = DateTimeOffset.Now + seconds;
invoice.ExpirationTime = expiry;
invoice.MonitoringExpiration = expiry.AddHours(1);
invoiceData.SetBlob(invoice);
await ctx.SaveChangesAsync();
_eventAggregator.Publish(new InvoiceDataChangedEvent(invoice));
_ = InvoiceNeedUpdateEventLater(invoiceId, seconds);
retry:
await using (var ctx = _applicationDbContextFactory.CreateContext())
{
var invoiceData = await ctx.Invoices.FindAsync(invoiceId);
var invoice = invoiceData.GetBlob(_btcPayNetworkProvider);
var expiry = DateTimeOffset.Now + seconds;
invoice.ExpirationTime = expiry;
invoice.MonitoringExpiration = expiry.AddHours(1);
invoiceData.SetBlob(invoice);
try
{
await ctx.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
goto retry;
}
_eventAggregator.Publish(new InvoiceDataChangedEvent(invoice));
_ = InvoiceNeedUpdateEventLater(invoiceId, seconds);
}
}
async Task InvoiceNeedUpdateEventLater(string invoiceId, TimeSpan expirationIn)
@ -166,13 +185,23 @@ namespace BTCPayServer.Services.Invoices
public async Task ExtendInvoiceMonitor(string invoiceId)
{
using var ctx = _applicationDbContextFactory.CreateContext();
var invoiceData = await ctx.Invoices.FindAsync(invoiceId);
retry:
using (var ctx = _applicationDbContextFactory.CreateContext())
{
var invoiceData = await ctx.Invoices.FindAsync(invoiceId);
var invoice = invoiceData.GetBlob(_btcPayNetworkProvider);
invoice.MonitoringExpiration = invoice.MonitoringExpiration.AddHours(1);
invoiceData.SetBlob(invoice);
await ctx.SaveChangesAsync();
var invoice = invoiceData.GetBlob(_btcPayNetworkProvider);
invoice.MonitoringExpiration = invoice.MonitoringExpiration.AddHours(1);
invoiceData.SetBlob(invoice);
try
{
await ctx.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
goto retry;
}
}
}
public async Task CreateInvoiceAsync(InvoiceEntity invoice, string[] additionalSearchTerms = null)
@ -279,62 +308,81 @@ namespace BTCPayServer.Services.Invoices
public async Task<bool> NewPaymentDetails(string invoiceId, IPaymentMethodDetails paymentMethodDetails, BTCPayNetworkBase network)
{
await using var context = _applicationDbContextFactory.CreateContext();
var invoice = (await context.Invoices.Where(i => i.Id == invoiceId).ToListAsync()).FirstOrDefault();
if (invoice == null)
return false;
retry:
await using (var context = _applicationDbContextFactory.CreateContext())
{
var invoice = (await context.Invoices.Where(i => i.Id == invoiceId).ToListAsync()).FirstOrDefault();
if (invoice == null)
return false;
var invoiceEntity = invoice.GetBlob(_btcPayNetworkProvider);
var paymentMethod = invoiceEntity.GetPaymentMethod(network, paymentMethodDetails.GetPaymentType());
if (paymentMethod == null)
return false;
var invoiceEntity = invoice.GetBlob(_btcPayNetworkProvider);
var paymentMethod = invoiceEntity.GetPaymentMethod(network, paymentMethodDetails.GetPaymentType());
if (paymentMethod == null)
return false;
var existingPaymentMethod = paymentMethod.GetPaymentMethodDetails();
paymentMethod.SetPaymentMethodDetails(paymentMethodDetails);
var existingPaymentMethod = paymentMethod.GetPaymentMethodDetails();
paymentMethod.SetPaymentMethodDetails(paymentMethodDetails);
#pragma warning disable CS0618
if (network.IsBTC)
{
invoiceEntity.DepositAddress = paymentMethod.DepositAddress;
}
if (network.IsBTC)
{
invoiceEntity.DepositAddress = paymentMethod.DepositAddress;
}
#pragma warning restore CS0618
invoiceEntity.SetPaymentMethod(paymentMethod);
invoice.SetBlob(invoiceEntity);
await context.AddressInvoices.AddAsync(new AddressInvoiceData()
{
InvoiceDataId = invoiceId,
CreatedTime = DateTimeOffset.UtcNow
}
.Set(GetDestination(paymentMethod), paymentMethod.GetId()));
AddToTextSearch(context, invoice, paymentMethodDetails.GetPaymentDestination());
await context.SaveChangesAsync();
return true;
}
public async Task UpdateInvoicePaymentMethod(string invoiceId, PaymentMethod paymentMethod)
{
using var context = _applicationDbContextFactory.CreateContext();
var invoice = await context.Invoices.FindAsync(invoiceId);
if (invoice == null)
return;
var network = paymentMethod.Network;
var invoiceEntity = invoice.GetBlob(_btcPayNetworkProvider);
var newDetails = paymentMethod.GetPaymentMethodDetails();
var existing = invoiceEntity.GetPaymentMethod(paymentMethod.GetId());
if (existing.GetPaymentMethodDetails().GetPaymentDestination() != newDetails.GetPaymentDestination() && newDetails.Activated)
{
invoiceEntity.SetPaymentMethod(paymentMethod);
invoice.SetBlob(invoiceEntity);
await context.AddressInvoices.AddAsync(new AddressInvoiceData()
{
InvoiceDataId = invoiceId,
CreatedTime = DateTimeOffset.UtcNow
}
.Set(GetDestination(paymentMethod), paymentMethod.GetId()));
.Set(GetDestination(paymentMethod), paymentMethod.GetId()));
AddToTextSearch(context, invoice, paymentMethodDetails.GetPaymentDestination());
try
{
await context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
goto retry;
}
return true;
}
}
public async Task UpdateInvoicePaymentMethod(string invoiceId, PaymentMethod paymentMethod)
{
retry:
using (var context = _applicationDbContextFactory.CreateContext())
{
var invoice = await context.Invoices.FindAsync(invoiceId);
if (invoice == null)
return;
var network = paymentMethod.Network;
var invoiceEntity = invoice.GetBlob(_btcPayNetworkProvider);
var newDetails = paymentMethod.GetPaymentMethodDetails();
var existing = invoiceEntity.GetPaymentMethod(paymentMethod.GetId());
if (existing.GetPaymentMethodDetails().GetPaymentDestination() != newDetails.GetPaymentDestination() && newDetails.Activated)
{
await context.AddressInvoices.AddAsync(new AddressInvoiceData()
{
InvoiceDataId = invoiceId,
CreatedTime = DateTimeOffset.UtcNow
}
.Set(GetDestination(paymentMethod), paymentMethod.GetId()));
}
invoiceEntity.SetPaymentMethod(paymentMethod);
invoice.SetBlob(invoiceEntity);
AddToTextSearch(context, invoice, paymentMethod.GetPaymentMethodDetails().GetPaymentDestination());
try
{
await context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
goto retry;
}
}
invoiceEntity.SetPaymentMethod(paymentMethod);
invoice.SetBlob(invoiceEntity);
AddToTextSearch(context, invoice, paymentMethod.GetPaymentMethodDetails().GetPaymentDestination());
await context.SaveChangesAsync();
}
public async Task AddPendingInvoiceIfNotPresent(string invoiceId)
@ -389,26 +437,38 @@ namespace BTCPayServer.Services.Invoices
public async Task UpdateInvoiceStatus(string invoiceId, InvoiceState invoiceState)
{
using var context = _applicationDbContextFactory.CreateContext();
var invoiceData = await context.FindAsync<Data.InvoiceData>(invoiceId).ConfigureAwait(false);
if (invoiceData == null)
return;
invoiceData.Status = InvoiceState.ToString(invoiceState.Status);
invoiceData.ExceptionStatus = InvoiceState.ToString(invoiceState.ExceptionStatus);
await context.SaveChangesAsync().ConfigureAwait(false);
await context.Database.GetDbConnection()
.ExecuteAsync("UPDATE \"Invoices\" SET \"Status\"=@status, \"ExceptionStatus\"=@exstatus WHERE \"Id\"=@id",
new
{
id = invoiceId,
status = InvoiceState.ToString(invoiceState.Status),
exstatus = InvoiceState.ToString(invoiceState.ExceptionStatus)
});
}
internal async Task UpdateInvoicePrice(string invoiceId, InvoiceEntity invoice)
internal async Task UpdateInvoicePrice(string invoiceId, decimal price)
{
if (invoice.Type != InvoiceType.TopUp)
throw new ArgumentException("The invoice type should be TopUp to be able to update invoice price", nameof(invoice));
using var context = _applicationDbContextFactory.CreateContext();
var invoiceData = await context.FindAsync<Data.InvoiceData>(invoiceId).ConfigureAwait(false);
if (invoiceData == null)
return;
var blob = invoiceData.GetBlob(_btcPayNetworkProvider);
blob.Price = invoice.Price;
AddToTextSearch(context, invoiceData, new[] { invoice.Price.ToString(CultureInfo.InvariantCulture) });
invoiceData.SetBlob(blob);
await context.SaveChangesAsync().ConfigureAwait(false);
retry:
using (var context = _applicationDbContextFactory.CreateContext())
{
var invoiceData = await context.FindAsync<Data.InvoiceData>(invoiceId).ConfigureAwait(false);
if (invoiceData == null)
return;
var blob = invoiceData.GetBlob(_btcPayNetworkProvider);
if (blob.Type != InvoiceType.TopUp)
throw new ArgumentException("The invoice type should be TopUp to be able to update invoice price", nameof(invoiceId));
blob.Price = price;
AddToTextSearch(context, invoiceData, new[] { price.ToString(CultureInfo.InvariantCulture) });
invoiceData.SetBlob(blob);
try
{
await context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
goto retry;
}
}
}
public async Task MassArchive(string[] invoiceIds, bool archive = true)
@ -436,37 +496,47 @@ namespace BTCPayServer.Services.Invoices
}
public async Task<InvoiceEntity> UpdateInvoiceMetadata(string invoiceId, string storeId, JObject metadata)
{
using var context = _applicationDbContextFactory.CreateContext();
var invoiceData = await GetInvoiceRaw(invoiceId, context);
if (invoiceData == null || (storeId != null &&
!invoiceData.StoreDataId.Equals(storeId,
StringComparison.InvariantCultureIgnoreCase)))
return null;
var blob = invoiceData.GetBlob(_btcPayNetworkProvider);
var newMetadata = InvoiceMetadata.FromJObject(metadata);
var oldOrderId = blob.Metadata.OrderId;
var newOrderId = newMetadata.OrderId;
if (newOrderId != oldOrderId)
retry:
using (var context = _applicationDbContextFactory.CreateContext())
{
// OrderId is saved in 2 places: (1) the invoice table and (2) in the metadata field. We are updating both for consistency.
invoiceData.OrderId = newOrderId;
var invoiceData = await GetInvoiceRaw(invoiceId, context);
if (invoiceData == null || (storeId != null &&
!invoiceData.StoreDataId.Equals(storeId,
StringComparison.InvariantCultureIgnoreCase)))
return null;
var blob = invoiceData.GetBlob(_btcPayNetworkProvider);
if (oldOrderId != null && (newOrderId is null || !newOrderId.Equals(oldOrderId, StringComparison.InvariantCulture)))
var newMetadata = InvoiceMetadata.FromJObject(metadata);
var oldOrderId = blob.Metadata.OrderId;
var newOrderId = newMetadata.OrderId;
if (newOrderId != oldOrderId)
{
RemoveFromTextSearch(context, invoiceData, oldOrderId);
// OrderId is saved in 2 places: (1) the invoice table and (2) in the metadata field. We are updating both for consistency.
invoiceData.OrderId = newOrderId;
if (oldOrderId != null && (newOrderId is null || !newOrderId.Equals(oldOrderId, StringComparison.InvariantCulture)))
{
RemoveFromTextSearch(context, invoiceData, oldOrderId);
}
if (newOrderId != null)
{
AddToTextSearch(context, invoiceData, new[] { newOrderId });
}
}
if (newOrderId != null)
blob.Metadata = newMetadata;
invoiceData.SetBlob(blob);
try
{
AddToTextSearch(context, invoiceData, new[] { newOrderId });
await context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
goto retry;
}
return ToEntity(invoiceData);
}
blob.Metadata = newMetadata;
invoiceData.SetBlob(blob);
await context.SaveChangesAsync().ConfigureAwait(false);
return ToEntity(invoiceData);
}
public async Task<bool> MarkInvoiceStatus(string invoiceId, InvoiceStatus status)
{

View File

@ -31,7 +31,7 @@
<div class="row">
<div class="col-xxl-constrain">
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div asp-validation-summary="ModelOnly"></div>
<div class="form-group" style="max-width:320px">
<label asp-for="Role" class="form-label"></label>

View File

@ -42,7 +42,7 @@
<input type="hidden" asp-for="StoreId" />
<input type="hidden" asp-for="Archived" />
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div asp-validation-summary="ModelOnly"></div>
<div class="row">
<div class="col-sm-10 col-md-9 col-xl-7 col-xxl-6">

View File

@ -27,7 +27,7 @@
<div class="col-xl-10 col-xxl-constrain">
@if (!ViewContext.ModelState.IsValid)
{
<div asp-validation-summary="All" class="text-danger"></div>
<div asp-validation-summary="All"></div>
}
<div class="form-group">
<label asp-for="Settings.Server" class="form-label">SMTP Server</label>

View File

@ -43,7 +43,7 @@
<input type="hidden" asp-for="StoreId" />
<input type="hidden" asp-for="Archived" />
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div asp-validation-summary="ModelOnly"></div>
<div class="row">
<div class="col-sm-10 col-md-9 col-xl-7 col-xxl-6">

View File

@ -10,7 +10,7 @@
</p>
<form asp-action="ForgotPassword" method="post">
<div asp-validation-summary="All" class="text-danger"></div>
<div asp-validation-summary="All"></div>
<div class="form-group">
<label asp-for="Email" class="form-label"></label>
<input asp-for="Email" class="form-control" />

View File

@ -9,7 +9,7 @@
<form asp-route-returnurl="@ViewData["ReturnUrl"]" method="post" id="login-form" asp-action="Login">
<fieldset disabled="@(ViewData.ContainsKey("disabled") ? "disabled" : null)">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div asp-validation-summary="ModelOnly"></div>
<div class="form-group">
<label asp-for="Email" class="form-label"></label>
<input asp-for="Email" class="form-control" required autofocus/>

View File

@ -3,7 +3,7 @@
<div class="twoFaBox">
<h2 class="h3 mb-3">Two-Factor Authentication</h2>
<form method="post" asp-route-returnUrl="@ViewData["ReturnUrl"]" asp-action="LoginWith2fa">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div asp-validation-summary="ModelOnly"></div>
<input asp-for="RememberMe" type="hidden"/>
<div class="form-group">
<label asp-for="TwoFactorCode" class="form-label"></label>

View File

@ -8,7 +8,7 @@
<form asp-route-returnUrl="@ViewData["ReturnUrl"]" asp-route-logon="true" method="post">
<fieldset disabled="@(ViewData.ContainsKey("disabled") ? "disabled" : null)" >
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div asp-validation-summary="ModelOnly"></div>
<div class="form-group">
<label asp-for="Email" class="form-label"></label>
<input asp-for="Email" class="form-control" required autofocus />

View File

@ -15,7 +15,7 @@
@if (Model.LoginWith2FaViewModel != null && Model.LoginWithFido2ViewModel != null&& Model.LoginWithLNURLAuthViewModel != null)
{
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div asp-validation-summary="ModelOnly"></div>
}
else if (Model.LoginWith2FaViewModel == null && Model.LoginWithFido2ViewModel == null && Model.LoginWithLNURLAuthViewModel == null)
{

View File

@ -5,7 +5,7 @@
}
<form method="post" asp-action="SetPassword">
<div asp-validation-summary="All" class="text-danger"></div>
<div asp-validation-summary="All"></div>
<input asp-for="Code" type="hidden"/>
<input asp-for="EmailSetInternally" type="hidden"/>
@if (Model.EmailSetInternally)

View File

@ -14,7 +14,7 @@
<div class="row">
<div class="col-xl-8 col-xxl-constrain">
<form asp-action="CreateApp" asp-route-appType="@Model.AppType">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div asp-validation-summary="ModelOnly"></div>
@if (string.IsNullOrEmpty(Model.AppType))
{
<div class="form-group">

View File

@ -20,7 +20,7 @@
<input asp-for="Config" type="hidden" />
@if (!ViewContext.ModelState.IsValid)
{
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div asp-validation-summary="ModelOnly"></div>
}
@if (!string.IsNullOrEmpty(Model.SelectedCustodian))
{

View File

@ -21,7 +21,7 @@
<input asp-for="Config" type="hidden" />
@if (!ViewContext.ModelState.IsValid)
{
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div asp-validation-summary="ModelOnly"></div>
}
<partial name="_FormTopMessages" model="Model.ConfigForm"/>

View File

@ -211,7 +211,7 @@
}
</div>
</div>
<div asp-validation-summary="All" class="text-danger"></div>
<div asp-validation-summary="All"></div>
<div class="form-group" style="max-width: 27rem;">
<label asp-for="Name" class="form-label" data-required></label>
<input asp-for="Name" class="form-control" required />

View File

@ -26,7 +26,7 @@
<main class="flex-grow-1">
@if (!ViewContext.ModelState.IsValid)
{
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div asp-validation-summary="ModelOnly"></div>
}
<partial name="_FormTopMessages" model="@Model.Form" />
<div class="d-flex flex-column justify-content-center gap-4">

View File

@ -38,7 +38,7 @@
<div class="row">
<div class="col-xl-8 col-xxl-constrain">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div asp-validation-summary="ModelOnly"></div>
@if (Model.StoreId != null)
{
<input type="hidden" asp-for="StoreId" />

View File

@ -17,7 +17,7 @@
@if (!ViewContext.ModelState.IsValid)
{
<div asp-validation-summary="All" class="text-danger"></div>
<div asp-validation-summary="All"></div>
}
@switch (Model.RefundStep)

View File

@ -15,7 +15,7 @@
<p>Set a schedule for automated Lightning Network Payouts.</p>
@if (!ViewContext.ModelState.IsValid)
{
<div asp-validation-summary="All" class="text-danger"></div>
<div asp-validation-summary="All"></div>
}
<form method="post">
<div class="form-check">

View File

@ -32,7 +32,7 @@
<p>Generate a new api key to use BTCPay through its API.</p>
<form method="post" asp-action="AddApiKey" id="Permissions">
<div asp-validation-summary="All" class="text-danger"></div>
<div asp-validation-summary="All"></div>
<div class="form-group">
<label asp-for="Label" class="form-label"></label>

View File

@ -45,7 +45,7 @@
<h1>@ViewData["Title"]</h1>
<p class="lead text-secondary mt-3">@(displayName ?? "An application") is requesting access to your BTCPay Server account.</p>
</header>
<div asp-validation-summary="All" class="text-danger"></div>
<div asp-validation-summary="All"></div>
@if (Model.NeedsStorePermission && store == null)
{

View File

@ -7,7 +7,7 @@
<div class="col-xl-8 col-xxl-constrain">
@if (!ViewContext.ModelState.IsValid)
{
<div asp-validation-summary="All" class="text-danger"></div>
<div asp-validation-summary="All"></div>
}
<form method="post">
<div class="form-group">

View File

@ -53,7 +53,7 @@
<span asp-validation-for="Code" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-primary mt-2">Verify</button>
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div asp-validation-summary="ModelOnly"></div>
</form>
</li>
</ol>

View File

@ -9,7 +9,7 @@
<form method="post">
@if (!ViewContext.ModelState.IsValid)
{
<div asp-validation-summary="All" class="text-danger"></div>
<div asp-validation-summary="All"></div>
}
<div class="form-group">
<div class="col-md-6">

View File

@ -12,7 +12,7 @@
<div class="row">
<div class="col-md-6">
<form method="post">
<div asp-validation-summary="All" class="text-danger"></div>
<div asp-validation-summary="All"></div>
<div class="form-group">
<label asp-for="NewPassword" class="form-label"></label>
<input asp-for="NewPassword" class="form-control" />

View File

@ -15,7 +15,7 @@
<p>Set a schedule for automated On-Chain Bitcoin Payouts. </p>
@if (!ViewContext.ModelState.IsValid)
{
<div asp-validation-summary="All" class="text-danger"></div>
<div asp-validation-summary="All"></div>
}
<form method="post">
<div class="form-check">

View File

@ -41,7 +41,7 @@
<div class="row">
<div class="col-xl-8 col-xxl-constrain">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div asp-validation-summary="ModelOnly"></div>
<div class="form-group">
<label asp-for="Title" class="form-label" data-required></label>
<input asp-for="Title" class="form-control" required />

View File

@ -35,7 +35,7 @@
<div class="row">
<div class="col-xl-8 col-xxl-constrain">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div asp-validation-summary="ModelOnly"></div>
<div class="form-group">
<label asp-for="Name" class="form-label" data-required></label>
<input asp-for="Name" class="form-control" required />

View File

@ -7,7 +7,7 @@
@if (!ViewContext.ModelState.IsValid)
{
<div asp-validation-summary="All" class="text-danger"></div>
<div asp-validation-summary="All"></div>
}
<div class="form-group">
<p>

View File

@ -7,7 +7,7 @@
<div class="row">
<div class="col-md-6">
<div asp-validation-summary="All" class="text-danger"></div>
<div asp-validation-summary="All"></div>
</div>
</div>

View File

@ -10,7 +10,7 @@
<div class="col-xl-8 col-xxl-constrain">
@if (!ViewContext.ModelState.IsValid)
{
<div asp-validation-summary="All" class="text-danger"></div>
<div asp-validation-summary="All"></div>
}
<form method="post">
<div class="form-group form-check">

View File

@ -8,7 +8,7 @@
<div class="row">
<div class="col-xl-6 col-xxl-constrain">
<form method="post" asp-action="CreateUser">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div asp-validation-summary="ModelOnly"></div>
<div class="form-group">
<label asp-for="Email" class="form-label"></label>

View File

@ -17,7 +17,7 @@
<div class="col-md-8">
@if (!ViewContext.ModelState.IsValid)
{
<div asp-validation-summary="All" class="text-danger"></div>
<div asp-validation-summary="All"></div>
}
<form method="post">
<div class="form-group">

View File

@ -9,7 +9,7 @@
<div class="col-md-8">
@if (!ViewContext.ModelState.IsValid)
{
<div asp-validation-summary="All" class="text-danger"></div>
<div asp-validation-summary="All"></div>
}
<div class="form-group">
<p>Lightning charge is a simple API for invoicing on lightning network, you can use it with several plugins:</p>

View File

@ -22,7 +22,7 @@
<div class="col-md-8">
@if (!ViewContext.ModelState.IsValid)
{
<div asp-validation-summary="All" class="text-danger"></div>
<div asp-validation-summary="All"></div>
}
<div class="form-group">
<h5>Browser connection</h5>

View File

@ -416,6 +416,12 @@
</form>
}
}
@if (disabled)
{
<form asp-action="UnInstallPlugin" asp-route-plugin="@plugin.Identifier">
<button type="submit" class="btn btn-sm btn-outline-danger">Uninstall</button>
</form>
}
</div>
</div>
</div>

View File

@ -20,7 +20,7 @@
@if (!ViewContext.ModelState.IsValid)
{
<div asp-validation-summary="All" class="text-danger"></div>
<div asp-validation-summary="All"></div>
}
<h4 class="mb-3">Full node connection</h4>

View File

@ -27,7 +27,7 @@
@if (!ViewContext.ModelState.IsValid)
{
<div asp-validation-summary="All" class="text-danger"></div>
<div asp-validation-summary="All"></div>
}
<div class="row">

View File

@ -20,7 +20,7 @@
@if (!ViewContext.ModelState.IsValid)
{
<div asp-validation-summary="All" class="text-danger"></div>
<div asp-validation-summary="All"></div>
}
<h4 class="mb-3">Full node connection</h4>

View File

@ -15,7 +15,7 @@
@if (!ViewContext.ModelState.IsValid)
{
<div asp-validation-summary="All" class="text-danger"></div>
<div asp-validation-summary="All"></div>
}
<div class="form-group">

View File

@ -16,7 +16,7 @@
<form method="post">
@if (!ViewContext.ModelState.IsValid)
{
<div asp-validation-summary="All" class="text-danger"></div>
<div asp-validation-summary="All"></div>
}
<div class="form-group">
<label asp-for="Provider" class="form-label"></label>

View File

@ -25,7 +25,7 @@
@if (!ViewContext.ModelState.IsValid)
{
<div asp-validation-summary="All" class="text-danger"></div>
<div asp-validation-summary="All"></div>
}
<form method="post" id="shopifyForm">

View File

@ -63,7 +63,7 @@
<form method="post" enctype="multipart/form-data">
@if (!ViewContext.ModelState.IsValid)
{
<div asp-validation-summary="All" class="text-danger"></div>
<div asp-validation-summary="All"></div>
}
<h3 class="mb-3">Invoice Settings</h3>
@if (Model.PaymentMethods.Any())

View File

@ -15,7 +15,7 @@
<div class="col-xxl-constrain col-xl-8">
@if (!ViewContext.ModelState.IsValid)
{
<div asp-validation-summary="All" class="text-danger"></div>
<div asp-validation-summary="All"></div>
}
<form method="post" enctype="multipart/form-data">
<h3 class="mb-3">General</h3>

View File

@ -17,7 +17,7 @@
@if (!ViewContext.ModelState.IsValid)
{
<div asp-validation-summary="All" class="text-danger"></div>
<div asp-validation-summary="All"></div>
}
<partial name="LocalhostBrowserSupport" />

View File

@ -17,7 +17,7 @@
@if (!ViewContext.ModelState.IsValid)
{
<div asp-validation-summary="All" class="text-danger"></div>
<div asp-validation-summary="All"></div>
}
<partial name="LocalhostBrowserSupport" />

View File

@ -19,7 +19,7 @@
@if (!ViewContext.ModelState.IsValid)
{
<div asp-validation-summary="All" class="text-danger"></div>
<div asp-validation-summary="All"></div>
}
<div class="my-5">

View File

@ -20,7 +20,7 @@
@try
{
var client = LightningClientFactoryService.Create(Model.ConnectionString, NetworkProvider.GetNetwork<BTCPayNetwork>(Model.CryptoCode));
client.GetDisplayName();
<span>@client.GetDisplayName()</span>
var uri = client.GetServerUri();
if (uri is not null)
{

View File

@ -20,7 +20,7 @@
@try
{
var client = LightningClientFactoryService.Create(Model.ConnectionString, NetworkProvider.GetNetwork<BTCPayNetwork>(Model.CryptoCode));
client.GetDisplayName();
<span>@client.GetDisplayName()</span>
var uri = client.GetServerUri();
if (uri is not null)
{

View File

@ -10,7 +10,7 @@
<h3 class="mb-3">@ViewData["Title"]</h3>
@if (!ViewContext.ModelState.IsValid)
{
<div asp-validation-summary="All" class="text-danger"></div>
<div asp-validation-summary="All"></div>
}
<form method="post">
<input type="hidden" asp-for="ShowScripting" />

View File

@ -156,11 +156,13 @@
</li>
</ul>
<p>
For the macaroon options you need to provide the <code>admin.macaroon</code>.<br/>
For the macaroon options you need to provide a macaroon with the <code>invoices:write</code> permission (e.g. <code>invoice.macaroon</code>, see
<a href="https://docs.lightning.engineering/lightning-network-tools/lnd/macaroons" target="_blank" rel="noreferrer noopener">details</a>). If you
want to display the node connection details, it also needs the <code>info:read</code> permission.<br/>
The path to the LND data directory may vary, the following examples assume <code>/root/.lnd</code>.
</p>
<p class="mb-2">The <code>macaroon</code> parameter expects the HEX value, it can be obtained using this command:</p>
<pre class="mb-4">xxd -p -c 256 /root/.lnd/data/chain/bitcoin/mainnet/admin.macaroon | tr -d '\n'</pre>
<pre class="mb-4">xxd -p -c 256 /root/.lnd/data/chain/bitcoin/mainnet/invoice.macaroon | tr -d '\n'</pre>
<p class="mb-2">
You can omit <code>certthumbprint</code> if the certificate is trusted by your machine.<br/>
The <code>certthumbprint</code> can be obtained using this command:

View File

@ -29,7 +29,7 @@
</div>
@if (!ViewContext.ModelState.IsValid)
{
<div asp-validation-summary="All" class="text-danger"></div>
<div asp-validation-summary="All"></div>
}
else
{

View File

@ -20,7 +20,7 @@
@if (!ViewContext.ModelState.IsValid)
{
<div asp-validation-summary="All" class="text-danger"></div>
<div asp-validation-summary="All"></div>
}
<form method="post">

View File

@ -1,7 +1,7 @@
@model BTCPayServer.Models.StoreViewModels.CreateStoreViewModel
<form asp-action="CreateStore">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div asp-validation-summary="ModelOnly"></div>
<div class="form-group">
<label asp-for="Name" class="form-label" data-required></label>
<input asp-for="Name" class="form-control w-300px" required />

View File

@ -37,7 +37,7 @@
<p class="mb-0">Otherwise you are exposing yourself to malicious site owners, or to malicious plugins installed in your browser.</p>
</div>
<div asp-validation-summary="All" class="text-danger"></div>
<div asp-validation-summary="All"></div>
<form method="post" asp-action="SignWithSeed" asp-route-walletId="@walletId">
<partial name="SigningContext" for="SigningContext"/>

View File

@ -27,6 +27,21 @@
visibility: hidden;
}
/* Form validation messages should match alert styles */
.validation-summary-errors {
padding: .75rem 1rem;
margin-bottom: 1.5rem;
color: var(--btcpay-danger-text);
background-color: var(--btcpay-danger);
border: var(--btcpay-border-width) solid var(--btcpay-danger-border);
border-radius: var(--btcpay-border-radius);
}
.alert > :last-child,
.validation-summary-errors > :last-child {
margin-bottom: 0;
}
/* General and site-wide Bootstrap modifications */
p {
margin-bottom: 1.5rem;

View File

@ -108,7 +108,8 @@
},
"tags": [
"Pull payments (Public)"
]
],
"security": []
}
},
"/api/v1/stores/{storeId}/pull-payments": {

View File

@ -1,5 +1,5 @@
<Project>
<PropertyGroup>
<Version>1.12.0</Version>
<Version>1.12.1</Version>
</PropertyGroup>
</Project>

View File

@ -1,5 +1,31 @@
# Changelog
## 1.12.1
Recommended update for users using Boltcard with pull payments or Top-Up invoices.
Breaking change: Boltcards linked to pull payments in version 1.12.0 are not compatible with version 1.12.1.
# New Features
* A disabled plugin can now be uninstalled in the UI (#5570) @Kukks
### Bug fixes
* Fix: Payments to Top-Up could go undetected due to a race condition (#5568) @NicolasDorier
* Lightning: Fixed the connection display name in LN settings (#5569) @dennisreimann
* Prevent redirection to archived store after login (#5566) @dennisreimann
* Use PullPaymentId to derive the cardkey of a Boltcard (#5575) @NicolasDorier
* Greenfield: The Link a boltcard to a pull payment route would not generate new keys for the boltcard when onExisting was set to UpdateVersion. @NicolasDorier
### Improvements
* Lightning Address: Use lowercase usernames when resolving (#5579) @dennisreimann
* UI: Form validation summary now matches alert style (#5576, #5564) @dennisreimann
* Improved error message in Vault if a hardware device isn't supported @NicolasDorier
* Lightning: Allow LND to be used with non-admin macaroons (#5567) @dennisreimann
* Fix in API Documentation: The Link a boltcard to a pull payment had incorrectly documented permissions. @NicolasDorier
## 1.12.0
### Noteworthy