WIP
This commit is contained in:
parent
ec215938df
commit
9c09a440e2
@ -42,14 +42,14 @@ public class LDKKVStore:KVStoreInterface
|
||||
public Result_NoneIOErrorZ write(string primary_namespace, string secondary_namespace, string key, byte[] buf)
|
||||
{
|
||||
var key1 = CombineKey(primary_namespace, secondary_namespace, key);
|
||||
_configProvider.Set(key1, buf).ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
_configProvider.Set(key1, buf, true).ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
return Result_NoneIOErrorZ.ok();
|
||||
}
|
||||
|
||||
public Result_NoneIOErrorZ remove(string primary_namespace, string secondary_namespace, string key, bool lazy)
|
||||
{
|
||||
var key1 = CombineKey(primary_namespace, secondary_namespace, key);
|
||||
_configProvider.Set<byte[]>(key1, null).ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
_configProvider.Set<byte[]>(key1, null, true).ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
return Result_NoneIOErrorZ.ok();
|
||||
}
|
||||
|
||||
|
||||
@ -202,7 +202,7 @@ public partial class LDKNode : IAsyncDisposable, IHostedService, IDisposable
|
||||
public async Task UpdateConfig(LightningConfig config)
|
||||
{
|
||||
await _started.Task;
|
||||
await _configProvider.Set(LightningConfig.Key, config);
|
||||
await _configProvider.Set(LightningConfig.Key, config, true);
|
||||
_config = config;
|
||||
|
||||
ConfigUpdated?.Invoke(this, config);
|
||||
@ -296,18 +296,18 @@ public partial class LDKNode : IAsyncDisposable, IHostedService, IDisposable
|
||||
|
||||
public async Task UpdateChannelManager(ChannelManager serializedChannelManager)
|
||||
{
|
||||
await _configProvider.Set("ln:ChannelManager", serializedChannelManager.write());
|
||||
await _configProvider.Set("ln:ChannelManager", serializedChannelManager.write(), true);
|
||||
}
|
||||
|
||||
|
||||
public async Task UpdateNetworkGraph(NetworkGraph networkGraph)
|
||||
{
|
||||
await _configProvider.Set("ln:NetworkGraph", networkGraph.write());
|
||||
await _configProvider.Set("ln:NetworkGraph", networkGraph.write(), true);
|
||||
}
|
||||
|
||||
public async Task UpdateScore(WriteableScore score)
|
||||
{
|
||||
await _configProvider.Set("ln:Score", score.write());
|
||||
await _configProvider.Set("ln:Score", score.write(), true);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -134,7 +134,7 @@ public class OnChainWalletManager : BaseHostedService
|
||||
walletConfig.Derivations[keyValuePair.Key].Identifier = keyValuePair.Value;
|
||||
|
||||
}
|
||||
await _configProvider.Set(WalletConfig.Key, walletConfig);
|
||||
await _configProvider.Set(WalletConfig.Key, walletConfig, true);
|
||||
WalletConfig = walletConfig;
|
||||
State = OnChainWalletState.Loaded;
|
||||
}
|
||||
@ -169,7 +169,7 @@ public class OnChainWalletManager : BaseHostedService
|
||||
Descriptor = descriptor,
|
||||
Identifier = result[key]
|
||||
};
|
||||
await _configProvider.Set(WalletConfig.Key, WalletConfig);
|
||||
await _configProvider.Set(WalletConfig.Key, WalletConfig, true);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@ -436,7 +436,7 @@ public class OnChainWalletManager : BaseHostedService
|
||||
|
||||
var updated = key.Aggregate(false, (current, k) => current || WalletConfig.Derivations.Remove(k));
|
||||
if (updated)
|
||||
await _configProvider.Set(WalletConfig.Key, WalletConfig);
|
||||
await _configProvider.Set(WalletConfig.Key, WalletConfig, true);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
@ -409,12 +409,12 @@ public class AuthStateProvider : AuthenticationStateProvider, IAccountManager, I
|
||||
|
||||
public async Task UpdateAccount(BTCPayAccount account)
|
||||
{
|
||||
await _config.Set(GetKey(account.Id), account);
|
||||
await _config.Set(GetKey(account.Id), account, true);
|
||||
}
|
||||
|
||||
public async Task RemoveAccount(BTCPayAccount account)
|
||||
{
|
||||
await _config.Set<BTCPayAccount>(GetKey(account.Id), null);
|
||||
await _config.Set<BTCPayAccount>(GetKey(account.Id), null, true);
|
||||
}
|
||||
|
||||
private async Task<BTCPayAccount> GetAccount(string serverUrl, string email)
|
||||
@ -435,7 +435,7 @@ public class AuthStateProvider : AuthenticationStateProvider, IAccountManager, I
|
||||
{
|
||||
OnBeforeAccountChange?.Invoke(this, _account);
|
||||
if (account != null) await UpdateAccount(account);
|
||||
await _config.Set(CurrentAccountKey, account?.Id);
|
||||
await _config.Set(CurrentAccountKey, account?.Id, true);
|
||||
_account = account;
|
||||
_userInfo = null;
|
||||
|
||||
|
||||
@ -14,6 +14,8 @@
|
||||
|
||||
<PackageReference Include="AsyncKeyedLock" Version="7.0.0" />
|
||||
|
||||
<PackageReference Include="FlexLabs.EntityFrameworkCore.Upsert" Version="8.0.0" />
|
||||
|
||||
<PackageReference Include="Laraue.EfCoreTriggers.SqlLite" Version="8.0.3" />
|
||||
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="8.0.6" />
|
||||
|
||||
@ -3,6 +3,6 @@
|
||||
public interface IConfigProvider
|
||||
{
|
||||
Task<T?> Get<T>(string key);
|
||||
Task Set<T>(string key, T? value);
|
||||
Task Set<T>(string key, T? value, bool backup);
|
||||
Task<IEnumerable<string>> List(string prefix);
|
||||
}
|
||||
@ -1,3 +1,8 @@
|
||||
namespace BTCPayApp.Core.Contracts;
|
||||
|
||||
public interface ISecureConfigProvider : IConfigProvider;
|
||||
public interface ISecureConfigProvider
|
||||
|
||||
{
|
||||
Task<T?> Get<T>(string key);
|
||||
Task Set<T>(string key, T? value);
|
||||
}
|
||||
@ -30,7 +30,7 @@ public class AppDbContext : DbContext
|
||||
modelBuilder.Entity<Outbox>()
|
||||
.HasKey(w => new {w.Entity, w.Key, w.ActionType, w.Version});
|
||||
modelBuilder.Entity<Outbox>().Property(payment => payment.Timestamp).HasDefaultValueSql("datetime('now')");
|
||||
|
||||
|
||||
modelBuilder.Entity<AppLightningPayment>().Property(payment => payment.PaymentRequest)
|
||||
.HasConversion(
|
||||
request => request.ToString(),
|
||||
@ -57,12 +57,12 @@ public class AppDbContext : DbContext
|
||||
|
||||
|
||||
//handling versioned data
|
||||
|
||||
|
||||
//settings, channels, payments
|
||||
|
||||
|
||||
//when creating, set the version to 0
|
||||
//when updating, increment the version
|
||||
|
||||
|
||||
// outbox creation
|
||||
// when creating, insert an outbox item
|
||||
// when updating, insert an outbox item
|
||||
@ -94,110 +94,110 @@ public class AppDbContext : DbContext
|
||||
ActionType = OutboxAction.Delete
|
||||
})))
|
||||
.AfterUpdate(trigger => trigger
|
||||
.Action(group => group.Update<Setting>(
|
||||
(tableRefs, setting) => tableRefs.Old.Key == setting.Key,
|
||||
(tableRefs, setting) => new Setting() {Version = tableRefs.Old.Version + 1})));
|
||||
// .AfterUpdate(trigger => trigger
|
||||
.Action(group => group
|
||||
// .Condition(@ref => @ref.Old.Value != @ref.New.Value)
|
||||
.Update<Setting>(
|
||||
(tableRefs, setting) => tableRefs.Old.Key == setting.Key,
|
||||
(tableRefs, setting) => new Setting() {Version = tableRefs.Old.Version + 1})
|
||||
.Insert(
|
||||
// .InsertIfNotExists( (@ref, outbox) => @ref.New.Version == outbox.Version && outbox.ActionType == OutboxAction.Update && outbox.Entity == "Setting" && outbox.Key == @ref.New.Key,
|
||||
@ref => new Outbox()
|
||||
{
|
||||
Entity = "Setting",
|
||||
Version = @ref.Old.Version + 1,
|
||||
Key = @ref.New.Key,
|
||||
ActionType = OutboxAction.Update
|
||||
})));
|
||||
// .Action(group => group
|
||||
// .Condition(@ref => @ref.Old.Backup)
|
||||
// .Condition(@ref => @ref.Old.Backup && !@ref.New.Backup)
|
||||
// .Insert(
|
||||
// // .InsertIfNotExists( (@ref, outbox) => @ref.New.Version == outbox.Version && outbox.ActionType == OutboxAction.Update && outbox.Entity == "Setting" && outbox.Key == @ref.New.Key,
|
||||
// // .InsertIfNotExists( (@ref, outbox) => @ref.New.Version == outbox.Version && outbox.ActionType == OutboxAction.Update && outbox.Entity == "Setting" && outbox.Key == @ref.New.Key,
|
||||
// @ref => new Outbox()
|
||||
// {
|
||||
// Entity = "Setting",
|
||||
// Version = @ref.New.Version,
|
||||
// Version = @ref.Old.Version +1,
|
||||
// Key = @ref.New.Key,
|
||||
// ActionType = OutboxAction.Update
|
||||
// ActionType = OutboxAction.Delete
|
||||
// })));
|
||||
|
||||
modelBuilder.Entity<Channel>()
|
||||
.AfterInsert(trigger => trigger
|
||||
.Action(group => group
|
||||
.Insert(
|
||||
// .InsertIfNotExists( (@ref, outbox) => outbox.Version == @ref.New.Version && outbox.ActionType == OutboxAction.Insert && outbox.Entity == "Channel" && outbox.Key == @ref.New.Id,
|
||||
@ref => new Outbox()
|
||||
{
|
||||
Entity = "Channel",
|
||||
Version = @ref.New.Version,
|
||||
Key = @ref.New.Id,
|
||||
ActionType = OutboxAction.Insert
|
||||
})))
|
||||
.AfterDelete(trigger => trigger
|
||||
.Action(group => group
|
||||
.Insert(
|
||||
// .InsertIfNotExists( (@ref, outbox) => @ref.Old.Version == outbox.Version && outbox.ActionType == OutboxAction.Delete && outbox.Entity == "Channel" && outbox.Key == @ref.Old.Id,
|
||||
@ref => new Outbox()
|
||||
{
|
||||
Entity = "Channel",
|
||||
Version = @ref.Old.Version,
|
||||
Key = @ref.Old.Id,
|
||||
ActionType = OutboxAction.Delete
|
||||
})))
|
||||
modelBuilder.Entity<Channel>()
|
||||
.AfterInsert(trigger => trigger
|
||||
.Action(group => group
|
||||
.Insert(
|
||||
// .InsertIfNotExists( (@ref, outbox) => outbox.Version == @ref.New.Version && outbox.ActionType == OutboxAction.Insert && outbox.Entity == "Channel" && outbox.Key == @ref.New.Id,
|
||||
@ref => new Outbox()
|
||||
{
|
||||
Entity = "Channel",
|
||||
Version = @ref.New.Version,
|
||||
Key = @ref.New.Id,
|
||||
ActionType = OutboxAction.Insert
|
||||
})))
|
||||
.AfterDelete(trigger => trigger
|
||||
.Action(group => group
|
||||
.Insert(
|
||||
// .InsertIfNotExists( (@ref, outbox) => @ref.Old.Version == outbox.Version && outbox.ActionType == OutboxAction.Delete && outbox.Entity == "Channel" && outbox.Key == @ref.Old.Id,
|
||||
@ref => new Outbox()
|
||||
{
|
||||
Entity = "Channel",
|
||||
Version = @ref.Old.Version,
|
||||
Key = @ref.Old.Id,
|
||||
ActionType = OutboxAction.Delete
|
||||
})))
|
||||
.AfterUpdate(trigger => trigger
|
||||
.Action(group => group.Update<Channel>(
|
||||
(tableRefs, setting) => tableRefs.Old.Id == setting.Id,
|
||||
(tableRefs, setting) => new Channel() {Version = tableRefs.Old.Version + 1}).Insert(
|
||||
// .InsertIfNotExists( (@ref, outbox) => @ref.New.Version == outbox.Version && outbox.ActionType == OutboxAction.Update && outbox.Entity == "Channel" && outbox.Key == @ref.New.Id,
|
||||
@ref => new Outbox()
|
||||
{
|
||||
Entity = "Channel",
|
||||
Version = @ref.Old.Version +1,
|
||||
Key = @ref.New.Id,
|
||||
ActionType = OutboxAction.Update
|
||||
})));
|
||||
|
||||
.AfterUpdate(trigger => trigger
|
||||
.Action(group => group.Update<Channel>(
|
||||
(tableRefs, setting) => tableRefs.Old.Id == setting.Id,
|
||||
(tableRefs, setting) => new Channel() {Version = tableRefs.Old.Version + 1})))
|
||||
;
|
||||
// .AfterUpdate(trigger => trigger
|
||||
// .Action(group => group
|
||||
// .Insert(
|
||||
// // .InsertIfNotExists( (@ref, outbox) => @ref.New.Version == outbox.Version && outbox.ActionType == OutboxAction.Update && outbox.Entity == "Channel" && outbox.Key == @ref.New.Id,
|
||||
// @ref => new Outbox()
|
||||
// {
|
||||
// Entity = "Channel",
|
||||
// Version = @ref.New.Version,
|
||||
// Key = @ref.New.Id,
|
||||
// ActionType = OutboxAction.Update
|
||||
// })));
|
||||
//
|
||||
modelBuilder.Entity<AppLightningPayment>()
|
||||
.AfterInsert(trigger => trigger
|
||||
.Action(group => group
|
||||
.Insert(
|
||||
// .InsertIfNotExists( (@ref, outbox) => outbox.Version == @ref.New.Version && outbox.ActionType == OutboxAction.Insert && outbox.Entity == "Payment" && outbox.Key == @ref.New.PaymentHash+ "_"+@ref.New.PaymentId+ "_"+@ref.New.Inbound,
|
||||
@ref => new Outbox()
|
||||
{
|
||||
Entity = "Payment",
|
||||
Version = @ref.New.Version,
|
||||
Key = @ref.New.PaymentHash + "_" + @ref.New.PaymentId + "_" + @ref.New.Inbound,
|
||||
ActionType = OutboxAction.Insert
|
||||
})))
|
||||
.AfterDelete(trigger => trigger
|
||||
.Action(group => group
|
||||
.Insert(
|
||||
// .InsertIfNotExists( (@ref, outbox) => @ref.Old.Version == outbox.Version && outbox.ActionType == OutboxAction.Delete && outbox.Entity == "Payment" && outbox.Key == @ref.Old.PaymentHash+ "_"+@ref.Old.PaymentId+ "_"+@ref.Old.Inbound,
|
||||
@ref => new Outbox()
|
||||
{
|
||||
Entity = "Payment",
|
||||
Version = @ref.Old.Version,
|
||||
Key = @ref.Old.PaymentHash + "_" + @ref.Old.PaymentId + "_" + @ref.Old.Inbound,
|
||||
ActionType = OutboxAction.Delete
|
||||
})))
|
||||
|
||||
.AfterUpdate(trigger => trigger
|
||||
.Action(group => group.Update<AppLightningPayment>(
|
||||
(tableRefs, setting) => tableRefs.Old.PaymentHash == setting.PaymentHash,
|
||||
(tableRefs, setting) => new AppLightningPayment() {Version = tableRefs.Old.Version + 1})))
|
||||
;
|
||||
// .AfterUpdate(trigger => trigger
|
||||
// .Action(group => group
|
||||
// .Insert(
|
||||
// // .InsertIfNotExists( (@ref, outbox) =>
|
||||
// // outbox.Version != @ref.New.Version || outbox.ActionType != OutboxAction.Update || outbox.Entity != "Payment" || outbox.Key != @ref.New.PaymentHash+ "_"+@ref.New.PaymentId+ "_"+@ref.New.Inbound,
|
||||
// @ref => new Outbox()
|
||||
// {
|
||||
// Entity = "Payment",
|
||||
// Version = @ref.New.Version,
|
||||
// Key = @ref.New.PaymentHash+ "_"+@ref.New.PaymentId+ "_"+@ref.New.Inbound,
|
||||
// ActionType = OutboxAction.Update
|
||||
// })));
|
||||
modelBuilder.Entity<AppLightningPayment>()
|
||||
.AfterInsert(trigger => trigger
|
||||
.Action(group => group
|
||||
.Insert(
|
||||
// .InsertIfNotExists( (@ref, outbox) => outbox.Version == @ref.New.Version && outbox.ActionType == OutboxAction.Insert && outbox.Entity == "Payment" && outbox.Key == @ref.New.PaymentHash+ "_"+@ref.New.PaymentId+ "_"+@ref.New.Inbound,
|
||||
@ref => new Outbox()
|
||||
{
|
||||
Entity = "Payment",
|
||||
Version = @ref.New.Version,
|
||||
Key = @ref.New.PaymentHash + "_" + @ref.New.PaymentId + "_" + @ref.New.Inbound,
|
||||
ActionType = OutboxAction.Insert
|
||||
})))
|
||||
.AfterDelete(trigger => trigger
|
||||
.Action(group => group
|
||||
.Insert(
|
||||
// .InsertIfNotExists( (@ref, outbox) => @ref.Old.Version == outbox.Version && outbox.ActionType == OutboxAction.Delete && outbox.Entity == "Payment" && outbox.Key == @ref.Old.PaymentHash+ "_"+@ref.Old.PaymentId+ "_"+@ref.Old.Inbound,
|
||||
@ref => new Outbox()
|
||||
{
|
||||
Entity = "Payment",
|
||||
Version = @ref.Old.Version,
|
||||
Key = @ref.Old.PaymentHash + "_" + @ref.Old.PaymentId + "_" + @ref.Old.Inbound,
|
||||
ActionType = OutboxAction.Delete
|
||||
})))
|
||||
.AfterUpdate(trigger => trigger
|
||||
.Action(group =>
|
||||
|
||||
group.Update<AppLightningPayment>(
|
||||
(tableRefs, setting) => tableRefs.Old.PaymentHash == setting.PaymentHash,
|
||||
(tableRefs, setting) => new AppLightningPayment() {Version = tableRefs.Old.Version + 1}).Insert(
|
||||
// .InsertIfNotExists( (@ref, outbox) =>
|
||||
// outbox.Version != @ref.New.Version || outbox.ActionType != OutboxAction.Update || outbox.Entity != "Payment" || outbox.Key != @ref.New.PaymentHash+ "_"+@ref.New.PaymentId+ "_"+@ref.New.Inbound,
|
||||
@ref => new Outbox()
|
||||
{
|
||||
Entity = "Payment",
|
||||
Version = @ref.Old.Version +1,
|
||||
Key = @ref.New.PaymentHash + "_" + @ref.New.PaymentId + "_" + @ref.New.Inbound,
|
||||
ActionType = OutboxAction.Update
|
||||
})));
|
||||
base.OnModelCreating(modelBuilder);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
public enum OutboxAction
|
||||
{
|
||||
Insert,
|
||||
@ -211,36 +211,5 @@ public class Outbox
|
||||
public OutboxAction ActionType { get; set; }
|
||||
public string Key { get; set; }
|
||||
public string Entity { get; set; }
|
||||
public ulong Version { get; set; }
|
||||
}
|
||||
|
||||
public class OutboxProcessor : IHostedService
|
||||
{
|
||||
private readonly IDbContextFactory<AppDbContext> _dbContextFactory;
|
||||
|
||||
public OutboxProcessor(IDbContextFactory<AppDbContext> dbContextFactory)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
}
|
||||
|
||||
private async Task ProcessOutbox(CancellationToken cancellationToken = default)
|
||||
{
|
||||
await using var db =
|
||||
new AppDbContext(new DbContextOptionsBuilder<AppDbContext>().UseSqlite("Data Source=outbox.db").Options);
|
||||
var outbox = db.Set<Outbox>();
|
||||
var outboxItems = await outbox.ToListAsync();
|
||||
foreach (var outboxItem in outboxItems)
|
||||
{
|
||||
// Process outbox item
|
||||
}
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
}
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
public long Version { get; set; }
|
||||
}
|
||||
@ -6,7 +6,7 @@ using NBitcoin;
|
||||
|
||||
namespace BTCPayApp.Core.Data;
|
||||
|
||||
public class AppLightningPayment : VersionedData
|
||||
public class AppLightningPayment : VersionedData<AppLightningPayment>
|
||||
{
|
||||
[JsonConverter(typeof(UInt256JsonConverter))]
|
||||
public uint256 PaymentHash { get; set; }
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
namespace BTCPayApp.Core.Data;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace BTCPayApp.Core.Data;
|
||||
|
||||
public class Channel:VersionedData
|
||||
{
|
||||
@ -12,5 +14,8 @@ public class ChannelAlias
|
||||
public string Id { get; set; }
|
||||
public string Type { get; set; }
|
||||
public string ChannelId { get; set; }
|
||||
[JsonIgnore]
|
||||
public Channel Channel { get; set; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -5,18 +5,19 @@ namespace BTCPayApp.Core.Data;
|
||||
public static class EFExtensions
|
||||
{
|
||||
|
||||
public static async Task<int> CrappyUpsert<T>(this DbContext ctx, T item, CancellationToken cancellationToken)
|
||||
public static async Task<int> Upsert<T>(this DbContext ctx, T item, CancellationToken cancellationToken) where T : class
|
||||
{
|
||||
ctx.Attach(item);
|
||||
ctx.Entry(item).State = EntityState.Modified;
|
||||
try
|
||||
{
|
||||
return await ctx.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
catch (DbUpdateException)
|
||||
{
|
||||
ctx.Entry(item).State = EntityState.Added;
|
||||
return await ctx.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
return await ctx.Upsert(item).RunAsync(cancellationToken);
|
||||
// ctx.Attach(item);
|
||||
// ctx.Entry(item).State = EntityState.Modified;
|
||||
// try
|
||||
// {
|
||||
// return await ctx.SaveChangesAsync(cancellationToken);
|
||||
// }
|
||||
// catch (DbUpdateException)
|
||||
// {
|
||||
// ctx.Entry(item).State = EntityState.Added;
|
||||
// return await ctx.SaveChangesAsync(cancellationToken);
|
||||
// }
|
||||
}
|
||||
}
|
||||
209
BTCPayApp.Core/Data/OutboxProcessor.cs
Normal file
209
BTCPayApp.Core/Data/OutboxProcessor.cs
Normal file
@ -0,0 +1,209 @@
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using BTCPayApp.Core.Attempt2;
|
||||
using BTCPayApp.Core.Contracts;
|
||||
using BTCPayApp.Core.Helpers;
|
||||
using Google.Protobuf;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NBitcoin;
|
||||
using VSSProto;
|
||||
|
||||
namespace BTCPayApp.Core.Data;
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
|
||||
public class SingleKeyDataProtector : IDataProtector
|
||||
{
|
||||
private readonly byte[] _key;
|
||||
|
||||
public SingleKeyDataProtector(byte[] key)
|
||||
{
|
||||
if (key.Length != 32) // AES-256 key size
|
||||
{
|
||||
throw new ArgumentException("Key length must be 32 bytes.");
|
||||
}
|
||||
|
||||
_key = key;
|
||||
}
|
||||
|
||||
public IDataProtector CreateProtector(string purpose)
|
||||
{
|
||||
using var hmac = new HMACSHA256(_key);
|
||||
var purposeBytes = Encoding.UTF8.GetBytes(purpose);
|
||||
var key = hmac.ComputeHash(purposeBytes).Take(32).ToArray();
|
||||
return new SingleKeyDataProtector(key);
|
||||
}
|
||||
|
||||
public byte[] Protect(byte[] plaintext)
|
||||
{
|
||||
using var aes = Aes.Create();
|
||||
aes.Key = _key;
|
||||
aes.GenerateIV();
|
||||
|
||||
byte[] iv = aes.IV;
|
||||
byte[] encrypted = aes.EncryptCbc(plaintext, iv);
|
||||
|
||||
byte[] result = new byte[iv.Length + encrypted.Length];
|
||||
Buffer.BlockCopy(iv, 0, result, 0, iv.Length);
|
||||
Buffer.BlockCopy(encrypted, 0, result, iv.Length, encrypted.Length);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public byte[] Unprotect(byte[] protectedData)
|
||||
{
|
||||
using var aes = Aes.Create();
|
||||
aes.Key = _key;
|
||||
|
||||
byte[] iv = new byte[16];
|
||||
byte[] cipherText = new byte[protectedData.Length - iv.Length];
|
||||
|
||||
Buffer.BlockCopy(protectedData, 0, iv, 0, iv.Length);
|
||||
Buffer.BlockCopy(protectedData, iv.Length, cipherText, 0, cipherText.Length);
|
||||
|
||||
return aes.DecryptCbc(cipherText, iv);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
public class OutboxProcessor : IScopedHostedService
|
||||
{
|
||||
private readonly IDbContextFactory<AppDbContext> _dbContextFactory;
|
||||
private readonly BTCPayConnectionManager _btcPayConnectionManager;
|
||||
private readonly ISecureConfigProvider _secureConfigProvider;
|
||||
|
||||
public OutboxProcessor(IDbContextFactory<AppDbContext> dbContextFactory, BTCPayConnectionManager btcPayConnectionManager, ISecureConfigProvider secureConfigProvider)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_btcPayConnectionManager = btcPayConnectionManager;
|
||||
_secureConfigProvider = secureConfigProvider;
|
||||
}
|
||||
|
||||
private async Task<IDataProtector> GetDataProtector()
|
||||
{
|
||||
var k = await _secureConfigProvider.Get<string>("encryptionKey");
|
||||
if (k == null)
|
||||
{
|
||||
k = Convert.ToHexString(RandomUtils.GetBytes(32)).ToLowerInvariant();
|
||||
await _secureConfigProvider.Set("encryptionKey", k);
|
||||
}
|
||||
|
||||
return new SingleKeyDataProtector(Convert.FromHexString(k));
|
||||
}
|
||||
|
||||
private async Task<KeyValue?> GetValue(AppDbContext dbContext, Outbox outbox)
|
||||
{
|
||||
var k = await _secureConfigProvider.Get<string>("encryptionKey");
|
||||
if (k == null)
|
||||
{
|
||||
k = Convert.ToHexString(RandomUtils.GetBytes(32)).ToLowerInvariant();
|
||||
await _secureConfigProvider.Set("encryptionKey", k);
|
||||
|
||||
}
|
||||
switch (outbox.Entity)
|
||||
{
|
||||
case "Setting":
|
||||
var setting = await dbContext.Settings.FindAsync(outbox.Key);
|
||||
if(setting?.Backup is not true)
|
||||
return null;
|
||||
return new KeyValue()
|
||||
{
|
||||
Key = outbox.Key,
|
||||
Value = ByteString.CopyFrom(setting.Value),
|
||||
Version = setting.Version
|
||||
};
|
||||
case "Channel":
|
||||
var channel = await dbContext.LightningChannels.Include(channel1 => channel1.Aliases).SingleOrDefaultAsync(channel1 => channel1.Id == outbox.Key);
|
||||
|
||||
if(channel == null)
|
||||
return null;
|
||||
var val = JsonSerializer.SerializeToUtf8Bytes(channel);
|
||||
return new KeyValue()
|
||||
{
|
||||
Key = outbox.Key,
|
||||
Value = ByteString.CopyFrom(val),
|
||||
Version = channel.Version
|
||||
};
|
||||
case "Payment":
|
||||
var split = outbox.Key.Split('_');
|
||||
var paymentHash = uint256.Parse(split[0]);
|
||||
var paymentId = split[1];
|
||||
var inbound = bool.Parse(split[2]);
|
||||
|
||||
var payment = await dbContext.LightningPayments.FindAsync(paymentHash, paymentId, inbound);
|
||||
if(payment == null)
|
||||
return null;
|
||||
var paymentBytes = JsonSerializer.SerializeToUtf8Bytes(payment);
|
||||
return new KeyValue()
|
||||
{
|
||||
Key = outbox.Key,
|
||||
Value = ByteString.CopyFrom(paymentBytes),
|
||||
Version = payment.Version
|
||||
};
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private async Task ProcessOutbox(CancellationToken cancellationToken = default)
|
||||
{
|
||||
await using var db = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
var putObjectRequest = new PutObjectRequest();
|
||||
var outbox = await db.OutboxItems.GroupBy(outbox1 => new {outbox1.Entity, outbox1.Key})
|
||||
.ToListAsync(cancellationToken: cancellationToken);
|
||||
foreach (var outboxItemSet in outbox)
|
||||
{
|
||||
var orderedEnumerable = outboxItemSet.OrderByDescending(outbox1 => outbox1.Version)
|
||||
.ThenByDescending(outbox1 => outbox1.ActionType).ToArray();
|
||||
foreach (var item in orderedEnumerable)
|
||||
{
|
||||
if (item.ActionType == OutboxAction.Delete )
|
||||
{
|
||||
putObjectRequest.DeleteItems.Add(new KeyValue()
|
||||
{
|
||||
Key = item.Entity, Version = item.Version
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
var kv = await GetValue(db, item);
|
||||
if (kv != null)
|
||||
{
|
||||
putObjectRequest.TransactionItems.Add(kv);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
db.OutboxItems.RemoveRange(orderedEnumerable);
|
||||
// Process outbox item
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private CancellationTokenSource _cts = new();
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_cts = new CancellationTokenSource();
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
while (!_cts.Token.IsCancellationRequested)
|
||||
{
|
||||
await ProcessOutbox(_cts.Token);
|
||||
await Task.Delay(2000, _cts.Token);
|
||||
}
|
||||
}, _cts.Token);
|
||||
}
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await _cts.CancelAsync();
|
||||
await ProcessOutbox(cancellationToken);
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,5 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace BTCPayApp.Core.Data;
|
||||
|
||||
|
||||
43
BTCPayApp.Core/Data/TLVHelper.cs
Normal file
43
BTCPayApp.Core/Data/TLVHelper.cs
Normal file
@ -0,0 +1,43 @@
|
||||
namespace BTCPayApp.Core.Data;
|
||||
|
||||
public static class TLVHelper
|
||||
{
|
||||
public record TLV(byte Tag, byte[] Value);
|
||||
|
||||
public static byte[] Write(List<TLV> tlvList)
|
||||
{
|
||||
List<byte> byteArray = new List<byte>();
|
||||
|
||||
foreach (var tlv in tlvList)
|
||||
{
|
||||
byteArray.Add(tlv.Tag);
|
||||
byteArray.AddRange(BitConverter.GetBytes(tlv.Value.Length));
|
||||
byteArray.AddRange(tlv.Value);
|
||||
}
|
||||
|
||||
return byteArray.ToArray();
|
||||
}
|
||||
|
||||
public static List<TLV> Read(byte[] byteArray)
|
||||
{
|
||||
var tlvList = new List<TLV>();
|
||||
var index = 0;
|
||||
|
||||
while (index < byteArray.Length)
|
||||
{
|
||||
var tag = byteArray[index];
|
||||
index += 1;
|
||||
|
||||
var length = BitConverter.ToInt32(byteArray, index);
|
||||
index += 4;
|
||||
|
||||
var value = new byte[length];
|
||||
Array.Copy(byteArray, index, value, 0, length);
|
||||
index += length;
|
||||
|
||||
tlvList.Add(new TLV(tag, value));
|
||||
}
|
||||
|
||||
return tlvList;
|
||||
}
|
||||
}
|
||||
@ -1,8 +1,9 @@
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace BTCPayApp.Core.Data;
|
||||
|
||||
public abstract class VersionedData
|
||||
{
|
||||
public ulong Version { get; set; } = 0;
|
||||
}
|
||||
public long Version { get; set; } = 0;
|
||||
}
|
||||
|
||||
@ -110,10 +110,10 @@ public class DatabaseConfigProvider: IConfigProvider
|
||||
return config is null ? default : JsonSerializer.Deserialize<T>(config.Value);
|
||||
}
|
||||
|
||||
public async Task Set<T>(string key, T? value)
|
||||
public async Task Set<T>(string key, T? value, bool backup)
|
||||
{
|
||||
using var releaser = await _lock.LockAsync(key);
|
||||
_logger.LogDebug("Setting {key} to {value}", key, value);
|
||||
_logger.LogDebug("Setting {key} to {value} {backup}", key, value, backup? "backup": "no backup");
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
if (value is null)
|
||||
{
|
||||
@ -129,8 +129,8 @@ public class DatabaseConfigProvider: IConfigProvider
|
||||
}
|
||||
|
||||
var newValue = typeof(T) == typeof(byte[])? value as byte[]:JsonSerializer.SerializeToUtf8Bytes(value);
|
||||
var setting = new Setting {Key = key, Value = newValue};
|
||||
await dbContext.CrappyUpsert(setting, CancellationToken.None);
|
||||
var setting = new Setting {Key = key, Value = newValue, Backup = backup};
|
||||
await dbContext.Upsert(setting, CancellationToken.None);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -297,8 +297,8 @@ var paySecret = new uint256(invoice.payment_secret());
|
||||
private async Task Payment(AppLightningPayment lightningPayment, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await using var context = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
var x = await context.CrappyUpsert(lightningPayment, cancellationToken);
|
||||
if (x > 0)
|
||||
var x = await context.Upsert(lightningPayment, cancellationToken);
|
||||
if (x > 1)//we have triggers that create an outbox record everytime so we need to check for more than 1 record
|
||||
{
|
||||
OnPaymentUpdate?.Invoke(this, lightningPayment);
|
||||
}
|
||||
|
||||
@ -11,7 +11,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
namespace BTCPayApp.Core.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
[Migration("20240723141157_triggers")]
|
||||
[Migration("20240724071302_triggers")]
|
||||
partial class triggers
|
||||
{
|
||||
/// <inheritdoc />
|
||||
@ -72,7 +72,7 @@ namespace BTCPayApp.Core.Migrations
|
||||
b
|
||||
.HasAnnotation("LC_TRIGGER_AFTER_DELETE_APPLIGHTNINGPAYMENT", "CREATE TRIGGER LC_TRIGGER_AFTER_DELETE_APPLIGHTNINGPAYMENT\r\nAFTER DELETE ON \"LightningPayments\"\r\nFOR EACH ROW\r\nBEGIN\r\n INSERT INTO \"OutboxItems\" (\"Entity\", \"Version\", \"Key\", \"ActionType\") SELECT 'Payment', \r\n OLD.\"Version\", \r\n OLD.\"PaymentHash\" || '_' || OLD.\"PaymentId\" || '_' || OLD.\"Inbound\", \r\n 2;\r\nEND;")
|
||||
.HasAnnotation("LC_TRIGGER_AFTER_INSERT_APPLIGHTNINGPAYMENT", "CREATE TRIGGER LC_TRIGGER_AFTER_INSERT_APPLIGHTNINGPAYMENT\r\nAFTER INSERT ON \"LightningPayments\"\r\nFOR EACH ROW\r\nBEGIN\r\n INSERT INTO \"OutboxItems\" (\"Entity\", \"Version\", \"Key\", \"ActionType\") SELECT 'Payment', \r\n NEW.\"Version\", \r\n NEW.\"PaymentHash\" || '_' || NEW.\"PaymentId\" || '_' || NEW.\"Inbound\", \r\n 0;\r\nEND;")
|
||||
.HasAnnotation("LC_TRIGGER_AFTER_UPDATE_APPLIGHTNINGPAYMENT", "CREATE TRIGGER LC_TRIGGER_AFTER_UPDATE_APPLIGHTNINGPAYMENT\r\nAFTER UPDATE ON \"LightningPayments\"\r\nFOR EACH ROW\r\nBEGIN\r\n UPDATE \"LightningPayments\"\r\n SET \"Version\" = OLD.\"Version\" + 1\r\n WHERE OLD.\"PaymentHash\" = \"LightningPayments\".\"PaymentHash\";\r\nEND;");
|
||||
.HasAnnotation("LC_TRIGGER_AFTER_UPDATE_APPLIGHTNINGPAYMENT", "CREATE TRIGGER LC_TRIGGER_AFTER_UPDATE_APPLIGHTNINGPAYMENT\r\nAFTER UPDATE ON \"LightningPayments\"\r\nFOR EACH ROW\r\nBEGIN\r\n UPDATE \"LightningPayments\"\r\n SET \"Version\" = OLD.\"Version\" + 1\r\n WHERE OLD.\"PaymentHash\" = \"LightningPayments\".\"PaymentHash\";\r\n INSERT INTO \"OutboxItems\" (\"Entity\", \"Version\", \"Key\", \"ActionType\") SELECT 'Payment', \r\n OLD.\"Version\" + 1, \r\n NEW.\"PaymentHash\" || '_' || NEW.\"PaymentId\" || '_' || NEW.\"Inbound\", \r\n 1;\r\nEND;");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayApp.Core.Data.Channel", b =>
|
||||
@ -101,7 +101,7 @@ namespace BTCPayApp.Core.Migrations
|
||||
b
|
||||
.HasAnnotation("LC_TRIGGER_AFTER_DELETE_CHANNEL", "CREATE TRIGGER LC_TRIGGER_AFTER_DELETE_CHANNEL\r\nAFTER DELETE ON \"LightningChannels\"\r\nFOR EACH ROW\r\nBEGIN\r\n INSERT INTO \"OutboxItems\" (\"Entity\", \"Version\", \"Key\", \"ActionType\") SELECT 'Channel', \r\n OLD.\"Version\", \r\n OLD.\"Id\", \r\n 2;\r\nEND;")
|
||||
.HasAnnotation("LC_TRIGGER_AFTER_INSERT_CHANNEL", "CREATE TRIGGER LC_TRIGGER_AFTER_INSERT_CHANNEL\r\nAFTER INSERT ON \"LightningChannels\"\r\nFOR EACH ROW\r\nBEGIN\r\n INSERT INTO \"OutboxItems\" (\"Entity\", \"Version\", \"Key\", \"ActionType\") SELECT 'Channel', \r\n NEW.\"Version\", \r\n NEW.\"Id\", \r\n 0;\r\nEND;")
|
||||
.HasAnnotation("LC_TRIGGER_AFTER_UPDATE_CHANNEL", "CREATE TRIGGER LC_TRIGGER_AFTER_UPDATE_CHANNEL\r\nAFTER UPDATE ON \"LightningChannels\"\r\nFOR EACH ROW\r\nBEGIN\r\n UPDATE \"LightningChannels\"\r\n SET \"Version\" = OLD.\"Version\" + 1\r\n WHERE OLD.\"Id\" = \"LightningChannels\".\"Id\";\r\nEND;");
|
||||
.HasAnnotation("LC_TRIGGER_AFTER_UPDATE_CHANNEL", "CREATE TRIGGER LC_TRIGGER_AFTER_UPDATE_CHANNEL\r\nAFTER UPDATE ON \"LightningChannels\"\r\nFOR EACH ROW\r\nBEGIN\r\n UPDATE \"LightningChannels\"\r\n SET \"Version\" = OLD.\"Version\" + 1\r\n WHERE OLD.\"Id\" = \"LightningChannels\".\"Id\";\r\n INSERT INTO \"OutboxItems\" (\"Entity\", \"Version\", \"Key\", \"ActionType\") SELECT 'Channel', \r\n OLD.\"Version\" + 1, \r\n NEW.\"Id\", \r\n 1;\r\nEND;");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayApp.Core.Data.ChannelAlias", b =>
|
||||
@ -141,7 +141,7 @@ namespace BTCPayApp.Core.Migrations
|
||||
b.Property<DateTimeOffset>("Timestamp")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValueSql("date()");
|
||||
.HasDefaultValueSql("datetime('now')");
|
||||
|
||||
b.HasKey("Entity", "Key", "ActionType", "Version");
|
||||
|
||||
@ -177,7 +177,7 @@ namespace BTCPayApp.Core.Migrations
|
||||
b
|
||||
.HasAnnotation("LC_TRIGGER_AFTER_DELETE_SETTING", "CREATE TRIGGER LC_TRIGGER_AFTER_DELETE_SETTING\r\nAFTER DELETE ON \"Settings\"\r\nFOR EACH ROW\r\nWHEN \r\n \r\n OLD.\"Backup\" IS TRUE\r\nBEGIN\r\n INSERT INTO \"OutboxItems\" (\"Entity\", \"Version\", \"Key\", \"ActionType\") SELECT 'Setting', \r\n OLD.\"Version\", \r\n OLD.\"Key\", \r\n 2;\r\nEND;")
|
||||
.HasAnnotation("LC_TRIGGER_AFTER_INSERT_SETTING", "CREATE TRIGGER LC_TRIGGER_AFTER_INSERT_SETTING\r\nAFTER INSERT ON \"Settings\"\r\nFOR EACH ROW\r\nWHEN \r\n \r\n NEW.\"Backup\" IS TRUE\r\nBEGIN\r\n INSERT INTO \"OutboxItems\" (\"Entity\", \"Version\", \"Key\", \"ActionType\") SELECT 'Setting', \r\n NEW.\"Version\", \r\n NEW.\"Key\", \r\n 0;\r\nEND;")
|
||||
.HasAnnotation("LC_TRIGGER_AFTER_UPDATE_SETTING", "CREATE TRIGGER LC_TRIGGER_AFTER_UPDATE_SETTING\r\nAFTER UPDATE ON \"Settings\"\r\nFOR EACH ROW\r\nBEGIN\r\n UPDATE \"Settings\"\r\n SET \"Version\" = OLD.\"Version\" + 1\r\n WHERE OLD.\"Key\" = \"Settings\".\"Key\";\r\nEND;");
|
||||
.HasAnnotation("LC_TRIGGER_AFTER_UPDATE_SETTING", "CREATE TRIGGER LC_TRIGGER_AFTER_UPDATE_SETTING\r\nAFTER UPDATE ON \"Settings\"\r\nFOR EACH ROW\r\nBEGIN\r\n UPDATE \"Settings\"\r\n SET \"Version\" = OLD.\"Version\" + 1\r\n WHERE OLD.\"Key\" = \"Settings\".\"Key\";\r\n INSERT INTO \"OutboxItems\" (\"Entity\", \"Version\", \"Key\", \"ActionType\") SELECT 'Setting', \r\n OLD.\"Version\" + 1, \r\n NEW.\"Key\", \r\n 1;\r\nEND;");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayApp.Core.Data.ChannelAlias", b =>
|
||||
@ -70,7 +70,7 @@ namespace BTCPayApp.Core.Migrations
|
||||
Key = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Entity = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Version = table.Column<ulong>(type: "INTEGER", nullable: false),
|
||||
Timestamp = table.Column<DateTimeOffset>(type: "TEXT", nullable: false, defaultValueSql: "date()")
|
||||
Timestamp = table.Column<DateTimeOffset>(type: "TEXT", nullable: false, defaultValueSql: "datetime('now')")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@ -86,19 +86,19 @@ namespace BTCPayApp.Core.Migrations
|
||||
|
||||
migrationBuilder.Sql("CREATE TRIGGER LC_TRIGGER_AFTER_INSERT_APPLIGHTNINGPAYMENT\r\nAFTER INSERT ON \"LightningPayments\"\r\nFOR EACH ROW\r\nBEGIN\r\n INSERT INTO \"OutboxItems\" (\"Entity\", \"Version\", \"Key\", \"ActionType\") SELECT 'Payment', \r\n NEW.\"Version\", \r\n NEW.\"PaymentHash\" || '_' || NEW.\"PaymentId\" || '_' || NEW.\"Inbound\", \r\n 0;\r\nEND;");
|
||||
|
||||
migrationBuilder.Sql("CREATE TRIGGER LC_TRIGGER_AFTER_UPDATE_APPLIGHTNINGPAYMENT\r\nAFTER UPDATE ON \"LightningPayments\"\r\nFOR EACH ROW\r\nBEGIN\r\n UPDATE \"LightningPayments\"\r\n SET \"Version\" = OLD.\"Version\" + 1\r\n WHERE OLD.\"PaymentHash\" = \"LightningPayments\".\"PaymentHash\";\r\nEND;");
|
||||
migrationBuilder.Sql("CREATE TRIGGER LC_TRIGGER_AFTER_UPDATE_APPLIGHTNINGPAYMENT\r\nAFTER UPDATE ON \"LightningPayments\"\r\nFOR EACH ROW\r\nBEGIN\r\n UPDATE \"LightningPayments\"\r\n SET \"Version\" = OLD.\"Version\" + 1\r\n WHERE OLD.\"PaymentHash\" = \"LightningPayments\".\"PaymentHash\";\r\n INSERT INTO \"OutboxItems\" (\"Entity\", \"Version\", \"Key\", \"ActionType\") SELECT 'Payment', \r\n OLD.\"Version\" + 1, \r\n NEW.\"PaymentHash\" || '_' || NEW.\"PaymentId\" || '_' || NEW.\"Inbound\", \r\n 1;\r\nEND;");
|
||||
|
||||
migrationBuilder.Sql("CREATE TRIGGER LC_TRIGGER_AFTER_DELETE_CHANNEL\r\nAFTER DELETE ON \"LightningChannels\"\r\nFOR EACH ROW\r\nBEGIN\r\n INSERT INTO \"OutboxItems\" (\"Entity\", \"Version\", \"Key\", \"ActionType\") SELECT 'Channel', \r\n OLD.\"Version\", \r\n OLD.\"Id\", \r\n 2;\r\nEND;");
|
||||
|
||||
migrationBuilder.Sql("CREATE TRIGGER LC_TRIGGER_AFTER_INSERT_CHANNEL\r\nAFTER INSERT ON \"LightningChannels\"\r\nFOR EACH ROW\r\nBEGIN\r\n INSERT INTO \"OutboxItems\" (\"Entity\", \"Version\", \"Key\", \"ActionType\") SELECT 'Channel', \r\n NEW.\"Version\", \r\n NEW.\"Id\", \r\n 0;\r\nEND;");
|
||||
|
||||
migrationBuilder.Sql("CREATE TRIGGER LC_TRIGGER_AFTER_UPDATE_CHANNEL\r\nAFTER UPDATE ON \"LightningChannels\"\r\nFOR EACH ROW\r\nBEGIN\r\n UPDATE \"LightningChannels\"\r\n SET \"Version\" = OLD.\"Version\" + 1\r\n WHERE OLD.\"Id\" = \"LightningChannels\".\"Id\";\r\nEND;");
|
||||
migrationBuilder.Sql("CREATE TRIGGER LC_TRIGGER_AFTER_UPDATE_CHANNEL\r\nAFTER UPDATE ON \"LightningChannels\"\r\nFOR EACH ROW\r\nBEGIN\r\n UPDATE \"LightningChannels\"\r\n SET \"Version\" = OLD.\"Version\" + 1\r\n WHERE OLD.\"Id\" = \"LightningChannels\".\"Id\";\r\n INSERT INTO \"OutboxItems\" (\"Entity\", \"Version\", \"Key\", \"ActionType\") SELECT 'Channel', \r\n OLD.\"Version\" + 1, \r\n NEW.\"Id\", \r\n 1;\r\nEND;");
|
||||
|
||||
migrationBuilder.Sql("CREATE TRIGGER LC_TRIGGER_AFTER_DELETE_SETTING\r\nAFTER DELETE ON \"Settings\"\r\nFOR EACH ROW\r\nWHEN \r\n \r\n OLD.\"Backup\" IS TRUE\r\nBEGIN\r\n INSERT INTO \"OutboxItems\" (\"Entity\", \"Version\", \"Key\", \"ActionType\") SELECT 'Setting', \r\n OLD.\"Version\", \r\n OLD.\"Key\", \r\n 2;\r\nEND;");
|
||||
|
||||
migrationBuilder.Sql("CREATE TRIGGER LC_TRIGGER_AFTER_INSERT_SETTING\r\nAFTER INSERT ON \"Settings\"\r\nFOR EACH ROW\r\nWHEN \r\n \r\n NEW.\"Backup\" IS TRUE\r\nBEGIN\r\n INSERT INTO \"OutboxItems\" (\"Entity\", \"Version\", \"Key\", \"ActionType\") SELECT 'Setting', \r\n NEW.\"Version\", \r\n NEW.\"Key\", \r\n 0;\r\nEND;");
|
||||
|
||||
migrationBuilder.Sql("CREATE TRIGGER LC_TRIGGER_AFTER_UPDATE_SETTING\r\nAFTER UPDATE ON \"Settings\"\r\nFOR EACH ROW\r\nBEGIN\r\n UPDATE \"Settings\"\r\n SET \"Version\" = OLD.\"Version\" + 1\r\n WHERE OLD.\"Key\" = \"Settings\".\"Key\";\r\nEND;");
|
||||
migrationBuilder.Sql("CREATE TRIGGER LC_TRIGGER_AFTER_UPDATE_SETTING\r\nAFTER UPDATE ON \"Settings\"\r\nFOR EACH ROW\r\nBEGIN\r\n UPDATE \"Settings\"\r\n SET \"Version\" = OLD.\"Version\" + 1\r\n WHERE OLD.\"Key\" = \"Settings\".\"Key\";\r\n INSERT INTO \"OutboxItems\" (\"Entity\", \"Version\", \"Key\", \"ActionType\") SELECT 'Setting', \r\n OLD.\"Version\" + 1, \r\n NEW.\"Key\", \r\n 1;\r\nEND;");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@ -69,7 +69,7 @@ namespace BTCPayApp.Core.Migrations
|
||||
b
|
||||
.HasAnnotation("LC_TRIGGER_AFTER_DELETE_APPLIGHTNINGPAYMENT", "CREATE TRIGGER LC_TRIGGER_AFTER_DELETE_APPLIGHTNINGPAYMENT\r\nAFTER DELETE ON \"LightningPayments\"\r\nFOR EACH ROW\r\nBEGIN\r\n INSERT INTO \"OutboxItems\" (\"Entity\", \"Version\", \"Key\", \"ActionType\") SELECT 'Payment', \r\n OLD.\"Version\", \r\n OLD.\"PaymentHash\" || '_' || OLD.\"PaymentId\" || '_' || OLD.\"Inbound\", \r\n 2;\r\nEND;")
|
||||
.HasAnnotation("LC_TRIGGER_AFTER_INSERT_APPLIGHTNINGPAYMENT", "CREATE TRIGGER LC_TRIGGER_AFTER_INSERT_APPLIGHTNINGPAYMENT\r\nAFTER INSERT ON \"LightningPayments\"\r\nFOR EACH ROW\r\nBEGIN\r\n INSERT INTO \"OutboxItems\" (\"Entity\", \"Version\", \"Key\", \"ActionType\") SELECT 'Payment', \r\n NEW.\"Version\", \r\n NEW.\"PaymentHash\" || '_' || NEW.\"PaymentId\" || '_' || NEW.\"Inbound\", \r\n 0;\r\nEND;")
|
||||
.HasAnnotation("LC_TRIGGER_AFTER_UPDATE_APPLIGHTNINGPAYMENT", "CREATE TRIGGER LC_TRIGGER_AFTER_UPDATE_APPLIGHTNINGPAYMENT\r\nAFTER UPDATE ON \"LightningPayments\"\r\nFOR EACH ROW\r\nBEGIN\r\n UPDATE \"LightningPayments\"\r\n SET \"Version\" = OLD.\"Version\" + 1\r\n WHERE OLD.\"PaymentHash\" = \"LightningPayments\".\"PaymentHash\";\r\nEND;");
|
||||
.HasAnnotation("LC_TRIGGER_AFTER_UPDATE_APPLIGHTNINGPAYMENT", "CREATE TRIGGER LC_TRIGGER_AFTER_UPDATE_APPLIGHTNINGPAYMENT\r\nAFTER UPDATE ON \"LightningPayments\"\r\nFOR EACH ROW\r\nBEGIN\r\n UPDATE \"LightningPayments\"\r\n SET \"Version\" = OLD.\"Version\" + 1\r\n WHERE OLD.\"PaymentHash\" = \"LightningPayments\".\"PaymentHash\";\r\n INSERT INTO \"OutboxItems\" (\"Entity\", \"Version\", \"Key\", \"ActionType\") SELECT 'Payment', \r\n OLD.\"Version\" + 1, \r\n NEW.\"PaymentHash\" || '_' || NEW.\"PaymentId\" || '_' || NEW.\"Inbound\", \r\n 1;\r\nEND;");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayApp.Core.Data.Channel", b =>
|
||||
@ -98,7 +98,7 @@ namespace BTCPayApp.Core.Migrations
|
||||
b
|
||||
.HasAnnotation("LC_TRIGGER_AFTER_DELETE_CHANNEL", "CREATE TRIGGER LC_TRIGGER_AFTER_DELETE_CHANNEL\r\nAFTER DELETE ON \"LightningChannels\"\r\nFOR EACH ROW\r\nBEGIN\r\n INSERT INTO \"OutboxItems\" (\"Entity\", \"Version\", \"Key\", \"ActionType\") SELECT 'Channel', \r\n OLD.\"Version\", \r\n OLD.\"Id\", \r\n 2;\r\nEND;")
|
||||
.HasAnnotation("LC_TRIGGER_AFTER_INSERT_CHANNEL", "CREATE TRIGGER LC_TRIGGER_AFTER_INSERT_CHANNEL\r\nAFTER INSERT ON \"LightningChannels\"\r\nFOR EACH ROW\r\nBEGIN\r\n INSERT INTO \"OutboxItems\" (\"Entity\", \"Version\", \"Key\", \"ActionType\") SELECT 'Channel', \r\n NEW.\"Version\", \r\n NEW.\"Id\", \r\n 0;\r\nEND;")
|
||||
.HasAnnotation("LC_TRIGGER_AFTER_UPDATE_CHANNEL", "CREATE TRIGGER LC_TRIGGER_AFTER_UPDATE_CHANNEL\r\nAFTER UPDATE ON \"LightningChannels\"\r\nFOR EACH ROW\r\nBEGIN\r\n UPDATE \"LightningChannels\"\r\n SET \"Version\" = OLD.\"Version\" + 1\r\n WHERE OLD.\"Id\" = \"LightningChannels\".\"Id\";\r\nEND;");
|
||||
.HasAnnotation("LC_TRIGGER_AFTER_UPDATE_CHANNEL", "CREATE TRIGGER LC_TRIGGER_AFTER_UPDATE_CHANNEL\r\nAFTER UPDATE ON \"LightningChannels\"\r\nFOR EACH ROW\r\nBEGIN\r\n UPDATE \"LightningChannels\"\r\n SET \"Version\" = OLD.\"Version\" + 1\r\n WHERE OLD.\"Id\" = \"LightningChannels\".\"Id\";\r\n INSERT INTO \"OutboxItems\" (\"Entity\", \"Version\", \"Key\", \"ActionType\") SELECT 'Channel', \r\n OLD.\"Version\" + 1, \r\n NEW.\"Id\", \r\n 1;\r\nEND;");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayApp.Core.Data.ChannelAlias", b =>
|
||||
@ -138,7 +138,7 @@ namespace BTCPayApp.Core.Migrations
|
||||
b.Property<DateTimeOffset>("Timestamp")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValueSql("date()");
|
||||
.HasDefaultValueSql("datetime('now')");
|
||||
|
||||
b.HasKey("Entity", "Key", "ActionType", "Version");
|
||||
|
||||
@ -174,7 +174,7 @@ namespace BTCPayApp.Core.Migrations
|
||||
b
|
||||
.HasAnnotation("LC_TRIGGER_AFTER_DELETE_SETTING", "CREATE TRIGGER LC_TRIGGER_AFTER_DELETE_SETTING\r\nAFTER DELETE ON \"Settings\"\r\nFOR EACH ROW\r\nWHEN \r\n \r\n OLD.\"Backup\" IS TRUE\r\nBEGIN\r\n INSERT INTO \"OutboxItems\" (\"Entity\", \"Version\", \"Key\", \"ActionType\") SELECT 'Setting', \r\n OLD.\"Version\", \r\n OLD.\"Key\", \r\n 2;\r\nEND;")
|
||||
.HasAnnotation("LC_TRIGGER_AFTER_INSERT_SETTING", "CREATE TRIGGER LC_TRIGGER_AFTER_INSERT_SETTING\r\nAFTER INSERT ON \"Settings\"\r\nFOR EACH ROW\r\nWHEN \r\n \r\n NEW.\"Backup\" IS TRUE\r\nBEGIN\r\n INSERT INTO \"OutboxItems\" (\"Entity\", \"Version\", \"Key\", \"ActionType\") SELECT 'Setting', \r\n NEW.\"Version\", \r\n NEW.\"Key\", \r\n 0;\r\nEND;")
|
||||
.HasAnnotation("LC_TRIGGER_AFTER_UPDATE_SETTING", "CREATE TRIGGER LC_TRIGGER_AFTER_UPDATE_SETTING\r\nAFTER UPDATE ON \"Settings\"\r\nFOR EACH ROW\r\nBEGIN\r\n UPDATE \"Settings\"\r\n SET \"Version\" = OLD.\"Version\" + 1\r\n WHERE OLD.\"Key\" = \"Settings\".\"Key\";\r\nEND;");
|
||||
.HasAnnotation("LC_TRIGGER_AFTER_UPDATE_SETTING", "CREATE TRIGGER LC_TRIGGER_AFTER_UPDATE_SETTING\r\nAFTER UPDATE ON \"Settings\"\r\nFOR EACH ROW\r\nBEGIN\r\n UPDATE \"Settings\"\r\n SET \"Version\" = OLD.\"Version\" + 1\r\n WHERE OLD.\"Key\" = \"Settings\".\"Key\";\r\n INSERT INTO \"OutboxItems\" (\"Entity\", \"Version\", \"Key\", \"ActionType\") SELECT 'Setting', \r\n OLD.\"Version\" + 1, \r\n NEW.\"Key\", \r\n 1;\r\nEND;");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayApp.Core.Data.ChannelAlias", b =>
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
using BTCPayApp.CommonServer;
|
||||
using System.Xml.Linq;
|
||||
using BTCPayApp.CommonServer;
|
||||
using BTCPayApp.Core.Attempt2;
|
||||
using BTCPayApp.Core.Auth;
|
||||
using BTCPayApp.Core.Contracts;
|
||||
@ -6,6 +7,8 @@ using BTCPayApp.Core.Data;
|
||||
using BTCPayApp.Core.LDK;
|
||||
using Laraue.EfCoreTriggers.SqlLite.Extensions;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using Microsoft.AspNetCore.DataProtection.Repositories;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
@ -39,12 +42,10 @@ public static class StartupExtensions
|
||||
serviceCollection.AddSingleton(sp => (IAccountManager)sp.GetRequiredService<AuthenticationStateProvider>());
|
||||
serviceCollection.AddSingleton<IConfigProvider, DatabaseConfigProvider>();
|
||||
serviceCollection.AddLDK();
|
||||
|
||||
return serviceCollection;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public class AppDatabaseMigrator: IHostedService
|
||||
{
|
||||
private readonly ILogger<AppDatabaseMigrator> _logger;
|
||||
|
||||
@ -15,7 +15,6 @@ public static class StartupExtensions
|
||||
serviceCollection.AddDataProtection(options =>
|
||||
{
|
||||
options.ApplicationDiscriminator = "BTCPayApp";
|
||||
|
||||
});
|
||||
serviceCollection.AddSingleton<IDataDirectoryProvider, DesktopDataDirectoryProvider>();
|
||||
// serviceCollection.AddSingleton<IConfigProvider, DesktopConfigProvider>();
|
||||
@ -25,56 +24,22 @@ public static class StartupExtensions
|
||||
}
|
||||
}
|
||||
|
||||
public class DesktopSecureConfigProvider: DesktopConfigProvider, ISecureConfigProvider
|
||||
public class DesktopSecureConfigProvider: ISecureConfigProvider
|
||||
{
|
||||
private readonly IDataProtector _dataProtector;
|
||||
|
||||
public DesktopSecureConfigProvider(IDataDirectoryProvider directoryProvider, IDataProtectionProvider dataProtectionProvider) : base(directoryProvider)
|
||||
public DesktopSecureConfigProvider(IDataDirectoryProvider directoryProvider, IDataProtectionProvider dataProtectionProvider)
|
||||
{
|
||||
_dataProtector = dataProtectionProvider.CreateProtector("SecureConfig");
|
||||
}
|
||||
|
||||
protected override Task<string> ReadFromRaw(string str) => Task.FromResult(_dataProtector.Unprotect(str));
|
||||
protected override Task<string> WriteFromRaw(string str) => Task.FromResult(_dataProtector.Protect(str));
|
||||
}
|
||||
|
||||
public class FingerprintProvider: IFingerprint
|
||||
{
|
||||
public Task<FingerprintAvailability> GetAvailabilityAsync(bool allowAlternativeAuthentication = false)
|
||||
{
|
||||
return Task.FromResult(FingerprintAvailability.NoImplementation);
|
||||
}
|
||||
|
||||
public Task<bool> IsAvailableAsync(bool allowAlternativeAuthentication = false)
|
||||
{
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
|
||||
public Task<FingerprintAuthenticationResult> AuthenticateAsync(AuthenticationRequestConfiguration authRequestConfig,
|
||||
CancellationToken cancellationToken = new CancellationToken())
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<AuthenticationType> GetAuthenticationTypeAsync()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
public class DesktopConfigProvider : IConfigProvider
|
||||
{
|
||||
private readonly Task<string> _configDir;
|
||||
|
||||
public DesktopConfigProvider(IDataDirectoryProvider directoryProvider)
|
||||
{
|
||||
_configDir = directoryProvider.GetAppDataDirectory().ContinueWith(task =>
|
||||
{
|
||||
var res = Path.Combine(task.Result, "config");
|
||||
Directory.CreateDirectory(res);
|
||||
return res;
|
||||
var res = Path.Combine(task.Result, "config");
|
||||
Directory.CreateDirectory(res);
|
||||
return res;
|
||||
});
|
||||
}
|
||||
|
||||
private readonly Task<string> _configDir;
|
||||
|
||||
public async Task<T?> Get<T>(string key)
|
||||
{
|
||||
@ -88,8 +53,6 @@ public class DesktopConfigProvider : IConfigProvider
|
||||
return JsonSerializer.Deserialize<T>(json);
|
||||
}
|
||||
|
||||
protected virtual Task<string> ReadFromRaw(string str) => Task.FromResult(str);
|
||||
protected virtual Task<string> WriteFromRaw(string str) => Task.FromResult(str);
|
||||
|
||||
public async Task Set<T>(string key, T? value)
|
||||
{
|
||||
@ -117,8 +80,34 @@ public class DesktopConfigProvider : IConfigProvider
|
||||
}
|
||||
return Directory.GetFiles(dir, $"{prefix}*").Select(Path.GetFileName).Where(p => p?.StartsWith(prefix) is true)!;
|
||||
}
|
||||
|
||||
protected Task<string> ReadFromRaw(string str) => Task.FromResult(_dataProtector.Unprotect(str));
|
||||
protected Task<string> WriteFromRaw(string str) => Task.FromResult(_dataProtector.Protect(str));
|
||||
}
|
||||
|
||||
public class FingerprintProvider: IFingerprint
|
||||
{
|
||||
public Task<FingerprintAvailability> GetAvailabilityAsync(bool allowAlternativeAuthentication = false)
|
||||
{
|
||||
return Task.FromResult(FingerprintAvailability.NoImplementation);
|
||||
}
|
||||
|
||||
public Task<bool> IsAvailableAsync(bool allowAlternativeAuthentication = false)
|
||||
{
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
|
||||
public Task<FingerprintAuthenticationResult> AuthenticateAsync(AuthenticationRequestConfiguration authRequestConfig,
|
||||
CancellationToken cancellationToken = new CancellationToken())
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<AuthenticationType> GetAuthenticationTypeAsync()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
public class DesktopDataDirectoryProvider : IDataDirectoryProvider
|
||||
{
|
||||
private readonly IConfiguration _configuration;
|
||||
|
||||
@ -77,7 +77,7 @@
|
||||
public async Task HandleValidSubmit()
|
||||
{
|
||||
_config!.Passcode = Model.NewPasscode;
|
||||
await ConfigProvider.Set(BTCPayAppConfig.Key, _config);
|
||||
await ConfigProvider.Set(BTCPayAppConfig.Key, _config, true);
|
||||
NavigationManager.NavigateTo(Routes.Settings);
|
||||
}
|
||||
|
||||
|
||||
@ -209,7 +209,7 @@
|
||||
if (HasPasscode)
|
||||
{
|
||||
_config!.Passcode = null;
|
||||
await ConfigProvider.Set(BTCPayAppConfig.Key, _config);
|
||||
await ConfigProvider.Set(BTCPayAppConfig.Key, _config, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -33,7 +33,7 @@ public class StateMiddleware(
|
||||
uiStateFeature.StateChanged += async (sender, args) =>
|
||||
{
|
||||
var state = (UIState)uiStateFeature.GetState() with { Instance = null };
|
||||
await configProvider.Set(UiStateConfigKey, state);
|
||||
await configProvider.Set(UiStateConfigKey, state, false);
|
||||
};
|
||||
|
||||
store.Initialized.ContinueWith(task => ListenIn(dispatcher));
|
||||
|
||||
@ -10,10 +10,10 @@ public class HttpVSSAPIClient : IVSSAPI
|
||||
private readonly Uri _endpoint;
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
private const string GET_OBJECT = "getObject";
|
||||
private const string PUT_OBJECTS = "putObjects";
|
||||
private const string DELETE_OBJECT = "deleteObject";
|
||||
private const string LIST_KEY_VERSIONS = "listKeyVersions";
|
||||
public const string GET_OBJECT = "getObject";
|
||||
public const string PUT_OBJECTS = "putObjects";
|
||||
public const string DELETE_OBJECT = "deleteObject";
|
||||
public const string LIST_KEY_VERSIONS = "listKeyVersions";
|
||||
public HttpVSSAPIClient(Uri endpoint, HttpClient? httpClient = null)
|
||||
{
|
||||
_endpoint = endpoint;
|
||||
|
||||
@ -18,9 +18,9 @@ public class ConcurrentMultiDictionary<TKey, TValue>
|
||||
|
||||
private readonly ConcurrentDictionary<TKey, Bag> _dictionary;
|
||||
|
||||
public ConcurrentMultiDictionary()
|
||||
public ConcurrentMultiDictionary(IEqualityComparer<TKey> invariantCultureIgnoreCase = null)
|
||||
{
|
||||
_dictionary = new ConcurrentDictionary<TKey, Bag>();
|
||||
_dictionary = new ConcurrentDictionary<TKey, Bag>(invariantCultureIgnoreCase);
|
||||
}
|
||||
|
||||
public int Count => _dictionary.Count;
|
||||
@ -41,7 +41,7 @@ public class ConcurrentMultiDictionary<TKey, TValue>
|
||||
|
||||
public bool AddOrReplace(TKey key, TValue value)
|
||||
{
|
||||
Remove(key, value);
|
||||
Remove(key, value, out _);
|
||||
return Add(key, value);
|
||||
}
|
||||
|
||||
@ -60,8 +60,9 @@ public class ConcurrentMultiDictionary<TKey, TValue>
|
||||
items = null;
|
||||
return false;
|
||||
}
|
||||
public bool Remove(TKey key, TValue value)
|
||||
public bool Remove(TKey key, TValue value, out bool keyRemoved)
|
||||
{
|
||||
keyRemoved = false;
|
||||
var spinWait = new SpinWait();
|
||||
while (true)
|
||||
{
|
||||
@ -82,7 +83,7 @@ public class ConcurrentMultiDictionary<TKey, TValue>
|
||||
}
|
||||
}
|
||||
if (spinAndRetry) { spinWait.SpinOnce(); continue; }
|
||||
bool keyRemoved = _dictionary.TryRemove(key, out var currentBag);
|
||||
keyRemoved = _dictionary.TryRemove(key, out var currentBag);
|
||||
Debug.Assert(keyRemoved, $"Key {key} was not removed");
|
||||
Debug.Assert(bag == currentBag, $"Removed wrong bag");
|
||||
return true;
|
||||
@ -134,6 +135,11 @@ public class ConcurrentMultiDictionary<TKey, TValue>
|
||||
{
|
||||
return _dictionary.Keys.Any(key => Contains(key, value));
|
||||
}
|
||||
|
||||
public bool ContainsValue(TKey key, TValue value)
|
||||
{
|
||||
return Contains(key, value);
|
||||
}
|
||||
public IEnumerable<TKey> GetKeysContainingValue(IEnumerable<TValue> value)
|
||||
{
|
||||
return _dictionary.Keys.Where(key => Contains(key, value));
|
||||
@ -142,4 +148,20 @@ public class ConcurrentMultiDictionary<TKey, TValue>
|
||||
{
|
||||
return _dictionary.Keys.Where(key => Contains(key, value));
|
||||
}
|
||||
|
||||
public bool RemoveValue(TValue value, out TKey[] keysRemoved)
|
||||
{
|
||||
List<TKey> keys = [];
|
||||
var anyValueRemoved = false;
|
||||
foreach (var key in GetKeysContainingValue(value))
|
||||
{
|
||||
anyValueRemoved = Remove(key, value, out var keyRemoved) || anyValueRemoved;
|
||||
if (keyRemoved)
|
||||
{
|
||||
keys.Add(key);
|
||||
}
|
||||
}
|
||||
keysRemoved = keys.ToArray();
|
||||
return anyValueRemoved;
|
||||
}
|
||||
}
|
||||
@ -4,6 +4,7 @@ using VSSProto;
|
||||
|
||||
namespace BTCPayApp.VSS;
|
||||
|
||||
|
||||
public class VSSApiEncryptorClient: IVSSAPI
|
||||
{
|
||||
private readonly IVSSAPI _vssApi;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user