Compare commits

...

18 Commits

Author SHA1 Message Date
Kukks
b4dea11bc6
Refactor the connection manager + sync 2024-07-30 15:57:17 +02:00
Kukks
f06cf66e81
Merge remote-tracking branch 'github.com/master' into jit-backups
# Conflicts:
#	submodules/btcpayserver
2024-07-29 13:38:12 +02:00
Kukks
e6ee0d8a2f
do not use syncer for now 2024-07-29 13:37:49 +02:00
Kukks
53fe9def9d
wip 2024-07-26 16:03:27 +02:00
Kukks
12ef00719e
restorer 2024-07-26 13:21:41 +02:00
Kukks
9b7a2ff7a2
moroe backup related code 2024-07-26 11:28:12 +02:00
Kukks
b4896685cb
Merge remote-tracking branch 'github.com/master' into jit-backups
# Conflicts:
#	BTCPayApp.UI/Pages/Settings/LightningPage.razor
2024-07-25 14:54:40 +02:00
Kukks
9c09a440e2
WIP 2024-07-25 14:50:25 +02:00
Kukks
ec215938df
wip 2024-07-23 16:28:35 +02:00
Kukks
a05d039a55
Merge remote-tracking branch 'github.com/master' into jit-backups
# Conflicts:
#	BTCPayApp.Core/Attempt2/BTCPayAppServerClient.cs
#	BTCPayApp.Core/Attempt2/BTCPayConnectionManager.cs
#	BTCPayApp.UI/StateMiddleware.cs
#	submodules/btcpayserver
2024-07-22 12:03:48 +02:00
Kukks
f7fe33a730
separate file 2024-07-22 11:54:06 +02:00
Kukks
e02ca2ba78
WIP (submodule not updated) 2024-07-10 22:33:59 +02:00
Kukks
ee3a83c235
wip triggers 2024-07-10 11:45:14 +02:00
Kukks
4526824fb4
wip 2024-07-10 11:45:13 +02:00
Kukks
9953264836
Refactor connection to isolate json frameworks and start backup 2024-07-10 11:45:13 +02:00
Kukks
c1788faefe
theoretically functional 2024-07-10 11:44:27 +02:00
Kukks
86c699d823
crying my way to success 2024-07-10 11:44:26 +02:00
Kukks
a75e6b9929
jit wip
subm
2024-07-10 11:44:26 +02:00
76 changed files with 2818 additions and 1032 deletions

View File

@ -0,0 +1,46 @@
using BTCPayApp.Core.Data;
using BTCPayServer.Lightning;
namespace BTCPayApp.Core.Attempt2;
public static class AppToServerHelper
{
public static LightningInvoice ToInvoice(this AppLightningPayment lightningPayment)
{
return new LightningInvoice()
{
Id = lightningPayment.PaymentHash.ToString(),
Amount = lightningPayment.Value,
PaymentHash = lightningPayment.PaymentHash.ToString(),
Preimage = lightningPayment.Preimage,
PaidAt = lightningPayment.Status == LightningPaymentStatus.Complete? DateTimeOffset.UtcNow: null, //TODO: store these in ln payment
BOLT11 = lightningPayment.PaymentRequest.ToString(),
Status = lightningPayment.Status == LightningPaymentStatus.Complete? LightningInvoiceStatus.Paid: lightningPayment.PaymentRequest.ExpiryDate < DateTimeOffset.UtcNow? LightningInvoiceStatus.Expired: LightningInvoiceStatus.Unpaid
};
}
public static LightningPayment ToPayment(this AppLightningPayment lightningPayment)
{
return new LightningPayment()
{
Id = lightningPayment.PaymentHash.ToString(),
Amount = LightMoney.MilliSatoshis(lightningPayment.Value),
PaymentHash = lightningPayment.PaymentHash.ToString(),
Preimage = lightningPayment.Preimage,
BOLT11 = lightningPayment.PaymentRequest.ToString(),
Status = lightningPayment.Status
};
}
public static async Task<List<LightningPayment>> ToPayments(this Task<List<AppLightningPayment>> appLightningPayments)
{
var result = await appLightningPayments;
return result.Select(ToPayment).ToList();
}
public static async Task<List<LightningInvoice>> ToInvoices(this Task<List<AppLightningPayment>> appLightningPayments)
{
var result = await appLightningPayments;
return result.Select(ToInvoice).ToList();
}
}

View File

@ -1,18 +1,16 @@
using System.Text;
using BTCPayApp.CommonServer;
using BTCPayApp.Core.Helpers;
using BTCPayApp.Core.LDK;
using BTCPayServer.Client.Models;
using BTCPayServer.Lightning;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using NBitcoin;
using NBitcoin.Crypto;
using LightningPayment = BTCPayApp.CommonServer.Models.LightningPayment;
namespace BTCPayApp.Core.Attempt2;
public class BTCPayAppServerClient(ILogger<BTCPayAppServerClient> logger, IServiceProvider serviceProvider) : IBTCPayAppHubClient
public class BTCPayAppServerClient(ILogger<BTCPayAppServerClient> _logger, IServiceProvider _serviceProvider) : IBTCPayAppHubClient
{
public event AsyncEventHandler<string>? OnNewBlock;
public event AsyncEventHandler<TransactionDetectedRequest>? OnTransactionDetected;
@ -20,99 +18,99 @@ public class BTCPayAppServerClient(ILogger<BTCPayAppServerClient> logger, IServi
public event AsyncEventHandler<string>? OnServerNodeInfo;
public event AsyncEventHandler<ServerEvent>? OnNotifyServerEvent;
public async Task NotifyServerEvent(ServerEvent serverEvent)
public async Task NotifyServerEvent(ServerEvent ev)
{
logger.LogInformation("NotifyServerEvent: {Type} - {Details}", serverEvent.Type, serverEvent.ToString());
await OnNotifyServerEvent?.Invoke(this, serverEvent)!;
_logger.LogInformation("NotifyServerEvent: {ev}", ev);
await OnNotifyServerEvent?.Invoke(this, ev);
}
public async Task NotifyNetwork(string network)
{
logger.LogInformation("NotifyNetwork: {network}", network);
_logger.LogInformation("NotifyNetwork: {network}", network);
await OnNotifyNetwork?.Invoke(this, network);
}
public async Task NotifyServerNode(string nodeInfo)
{
logger.LogInformation("NotifyServerNode: {nodeInfo}", nodeInfo);
_logger.LogInformation("NotifyServerNode: {nodeInfo}", nodeInfo);
await OnServerNodeInfo?.Invoke(this, nodeInfo);
}
public async Task TransactionDetected(TransactionDetectedRequest request)
{
logger.LogInformation($"OnTransactionDetected: {request.TxId}");
_logger.LogInformation($"OnTransactionDetected: {request.TxId}");
await OnTransactionDetected?.Invoke(this, request);
}
public async Task NewBlock(string block)
{
logger.LogInformation("NewBlock: {block}", block);
_logger.LogInformation("NewBlock: {block}", block);
await OnNewBlock?.Invoke(this, block);
}
private PaymentsManager PaymentsManager =>
serviceProvider.GetRequiredService<LightningNodeManager>().Node.PaymentsManager;
_serviceProvider.GetRequiredService<LightningNodeManager>().Node.PaymentsManager;
public async Task<LightningPayment> CreateInvoice(CreateLightningInvoiceRequest createLightningInvoiceRequest)
public async Task<LightningInvoice> CreateInvoice(CreateLightningInvoiceRequest createLightningInvoiceRequest)
{
var descHash = new uint256(Hashes.SHA256(Encoding.UTF8.GetBytes(createLightningInvoiceRequest.Description)),
false);
return await PaymentsManager.RequestPayment(createLightningInvoiceRequest.Amount,
createLightningInvoiceRequest.Expiry, descHash);
return (await PaymentsManager.RequestPayment(createLightningInvoiceRequest.Amount,
createLightningInvoiceRequest.Expiry, descHash)).ToInvoice();
}
public async Task<LightningPayment?> GetLightningInvoice(string paymentHash)
public async Task<LightningInvoice?> GetLightningInvoice(uint256 paymentHash)
{
var invs = await PaymentsManager.List(payments =>
payments.Where(payment => payment.Inbound && payment.PaymentHash == paymentHash));
return invs.FirstOrDefault();
return invs.FirstOrDefault()?.ToInvoice();
}
public async Task<LightningPayment?> GetLightningPayment(string paymentHash)
public async Task<LightningPayment?> GetLightningPayment(uint256 paymentHash)
{
var invs = await PaymentsManager.List(payments =>
payments.Where(payment => !payment.Inbound && payment.PaymentHash == paymentHash));
return invs.FirstOrDefault();
return invs.FirstOrDefault()?.ToPayment();
}
public async Task<List<LightningPayment>> GetLightningPayments(ListPaymentsParams request)
{
return await PaymentsManager.List(payments => payments.Where(payment => !payment.Inbound), default);
return await PaymentsManager.List(payments => payments.Where(payment => !payment.Inbound), default).ToPayments();
}
public async Task<List<LightningPayment>> GetLightningInvoices(ListInvoicesParams request)
public async Task<List<LightningInvoice>> GetLightningInvoices(ListInvoicesParams request)
{
return await PaymentsManager.List(payments => payments.Where(payment => payment.Inbound), default);
return await PaymentsManager.List(payments => payments.Where(payment => payment.Inbound), default).ToInvoices();
}
public async Task<PayResponse> PayInvoice(string bolt11, long? amountMilliSatoshi)
{
var network = serviceProvider.GetRequiredService<OnChainWalletManager>().Network;
var network = _serviceProvider.GetRequiredService<OnChainWalletManager>().Network;
var bolt = BOLT11PaymentRequest.Parse(bolt11, network);
try
{
var result = await PaymentsManager.PayInvoice(bolt,
amountMilliSatoshi is null ? null : LightMoney.MilliSatoshis(amountMilliSatoshi.Value));
return new PayResponse()
{
Result = result.Status switch
{
Result = result.Status switch
{
LightningPaymentStatus.Unknown => PayResult.Unknown,
LightningPaymentStatus.Pending => PayResult.Unknown,
LightningPaymentStatus.Complete => PayResult.Ok,
LightningPaymentStatus.Failed => PayResult.Error,
_ => throw new ArgumentOutOfRangeException()
},
Details = new PayDetails()
{
Preimage = result.Preimage is not null ? new uint256(result.Preimage) : null,
Status = result.Status
}
};
LightningPaymentStatus.Unknown => PayResult.Unknown,
LightningPaymentStatus.Pending => PayResult.Unknown,
LightningPaymentStatus.Complete => PayResult.Ok,
LightningPaymentStatus.Failed => PayResult.Error,
_ => throw new ArgumentOutOfRangeException()
},
Details = new PayDetails()
{
Preimage = result.Preimage is not null ? new uint256(result.Preimage) : null,
Status = result.Status
}
};
}
catch (Exception e)
{
logger.LogError(e, "Error paying invoice");
_logger.LogError(e, "Error paying invoice");
return new PayResponse(PayResult.Error, e.Message);
}
}

View File

@ -1,6 +1,7 @@
using System.Net;
using BTCPayApp.CommonServer;
using BTCPayApp.Core.Auth;
using BTCPayApp.Core.Contracts;
using BTCPayApp.Core.Helpers;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.SignalR.Client;
@ -14,25 +15,28 @@ namespace BTCPayApp.Core.Attempt2;
public class BTCPayConnectionManager : IHostedService, IHubConnectionObserver
{
private const string ConfigDeviceIdentifierKey = "deviceIdentifier";
private readonly IAccountManager _accountManager;
private readonly AuthenticationStateProvider _authStateProvider;
private readonly ILogger<BTCPayConnectionManager> _logger;
private readonly BTCPayAppServerClient _btcPayAppServerClient;
private readonly IBTCPayAppHubClient _btcPayAppServerClientInterface;
private readonly IConfigProvider _configProvider;
private readonly SyncService _syncService;
private IDisposable? _subscription;
public IBTCPayAppHubServer? HubProxy { get; private set; }
public HubConnection? Connection { get; private set; }
private HubConnection? Connection { get; set; }
public Network? ReportedNetwork { get; private set; }
public string ReportedNodeInfo { get; set; }
public event AsyncEventHandler<(HubConnectionState Old, HubConnectionState New)>? ConnectionChanged;
private HubConnectionState _connectionState = HubConnectionState.Disconnected;
public event AsyncEventHandler<(BTCPayConnectionState Old, BTCPayConnectionState New)>? ConnectionChanged;
private BTCPayConnectionState _connectionState = BTCPayConnectionState.Init;
public HubConnectionState ConnectionState
public BTCPayConnectionState ConnectionState
{
get => Connection?.State ?? HubConnectionState.Disconnected;
get => _connectionState;
private set
{
if (_connectionState == value)
@ -49,25 +53,123 @@ public class BTCPayConnectionManager : IHostedService, IHubConnectionObserver
AuthenticationStateProvider authStateProvider,
ILogger<BTCPayConnectionManager> logger,
BTCPayAppServerClient btcPayAppServerClient,
IBTCPayAppHubClient btcPayAppServerClientInterface)
IBTCPayAppHubClient btcPayAppServerClientInterface,
IConfigProvider configProvider,
SyncService syncService)
{
_accountManager = accountManager;
_authStateProvider = authStateProvider;
_logger = logger;
_btcPayAppServerClient = btcPayAppServerClient;
_btcPayAppServerClientInterface = btcPayAppServerClientInterface;
_configProvider = configProvider;
_syncService = syncService;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
ConnectionChanged += OnConnectionChanged;
_authStateProvider.AuthenticationStateChanged += OnAuthenticationStateChanged;
_btcPayAppServerClient.OnNotifyNetwork += OnNotifyNetwork;
_btcPayAppServerClient.OnNotifyServerEvent += OnNotifyServerEvent;
_btcPayAppServerClient.OnServerNodeInfo += OnServerNodeInfo;
await StartOrReplace();
_ = TryStayConnected();
await OnConnectionChanged(this, (BTCPayConnectionState.Init, BTCPayConnectionState.Init));
}
private async Task<long> GetDeviceIdentifier()
{
return await _configProvider.GetOrSet(ConfigDeviceIdentifierKey,
async () => RandomUtils.GetInt64(), false);
}
private async Task OnConnectionChanged(object? sender, (BTCPayConnectionState Old, BTCPayConnectionState New) e)
{
var account = _accountManager.GetAccount();
switch (e.New)
{
case BTCPayConnectionState.Init:
ConnectionState = BTCPayConnectionState.WaitingForAuth;
break;
case BTCPayConnectionState.WaitingForAuth:
await Kill();
if (account is not null)
{
ConnectionState = BTCPayConnectionState.Connecting;
}
break;
case BTCPayConnectionState.Connecting:
if (account is null)
{
ConnectionState = BTCPayConnectionState.WaitingForAuth;
break;
}
if (Connection is null)
{
Connection = new HubConnectionBuilder()
.AddNewtonsoftJsonProtocol(options =>
{
NBitcoin.JsonConverters.Serializer.RegisterFrontConverters(
options.PayloadSerializerSettings);
})
.WithUrl(new Uri(new Uri(account.BaseUri), "hub/btcpayapp").ToString(),
options =>
{
options.AccessTokenProvider = () =>
Task.FromResult(_accountManager.GetAccount()?.AccessToken);
})
.WithAutomaticReconnect()
.Build();
_subscription = Connection.Register(_btcPayAppServerClientInterface);
HubProxy = Connection.CreateHubProxy<IBTCPayAppHubServer>();
}
if (Connection.State == HubConnectionState.Disconnected)
{
try
{
await Connection.StartAsync();
if (Connection.State == HubConnectionState.Connected)
{
ConnectionState = BTCPayConnectionState.Syncing;
}
}
catch (HttpRequestException ex) when (ex.StatusCode is HttpStatusCode.Unauthorized)
{
await _accountManager.RefreshAccess();
ConnectionState = BTCPayConnectionState.WaitingForAuth;
}
}
break;
case BTCPayConnectionState.Syncing:
await _syncService.SyncToLocal();
ConnectionState = BTCPayConnectionState.ConnectedFinishedInitialSync;
break;
case BTCPayConnectionState.ConnectedFinishedInitialSync:
var deviceIdentifier = await GetDeviceIdentifier();
var master = await HubProxy.DeviceMasterSignal(deviceIdentifier, true);
ConnectionState =
master ? BTCPayConnectionState.ConnectedAsMaster : BTCPayConnectionState.ConnectedAsSlave;
break;
case BTCPayConnectionState.ConnectedAsMaster:
await _syncService.StartSync(false, await GetDeviceIdentifier());
break;
case BTCPayConnectionState.ConnectedAsSlave:
await _syncService.StartSync(true, await GetDeviceIdentifier());
break;
case BTCPayConnectionState.Disconnected:
ConnectionState = BTCPayConnectionState.WaitingForAuth;
break;
}
}
private async Task OnServerNodeInfo(object? sender, string e)
{
ReportedNodeInfo = e;
@ -89,10 +191,8 @@ public class BTCPayConnectionManager : IHostedService, IHubConnectionObserver
{
await task;
var authenticated = await _accountManager.CheckAuthenticated();
if (!authenticated)
await Kill();
else
await StartOrReplace();
await Kill();
ConnectionState = !authenticated ? BTCPayConnectionState.WaitingForAuth : BTCPayConnectionState.Connecting;
}
catch (Exception e)
{
@ -100,92 +200,55 @@ public class BTCPayConnectionManager : IHostedService, IHubConnectionObserver
}
}
private async Task TryStayConnected()
{
while (true)
{
try
{
if (Connection is not null && ConnectionState == HubConnectionState.Disconnected)
{
await Connection.StartAsync();
ConnectionState = HubConnectionState.Connected;
}
else
{
await Task.Delay(5000);
}
}
catch (HttpRequestException ex) when (ex.StatusCode is HttpStatusCode.Unauthorized)
{
var result = await _accountManager.RefreshAccess();
if (result.Succeeded)
await StartOrReplace();
else
await Kill();
await Task.Delay(1000);
}
catch (Exception e)
{
await Task.Delay(1000);
}
}
}
private async Task Kill()
{
if (Connection is not null)
await Connection.StopAsync();
var conn = Connection;
Connection = null;
ConnectionState = HubConnectionState.Disconnected;
if (conn is not null)
await conn.StopAsync();
_subscription?.Dispose();
_subscription = null;
HubProxy = null;
await _syncService.StopSync();
}
private async Task StartOrReplace()
public async Task StopAsync(CancellationToken cancellationToken)
{
if (_connectionState == BTCPayConnectionState.ConnectedAsMaster)
{
_logger.LogInformation("Sending device master signal to turn off");
var deviceIdentifier = await GetDeviceIdentifier();
await HubProxy.DeviceMasterSignal(deviceIdentifier, true);
}
await Kill();
var account = _accountManager.GetAccount();
if (account is null)
return;
Connection = new HubConnectionBuilder()
.AddNewtonsoftJsonProtocol()
.WithUrl(new Uri(new Uri(account.BaseUri), "hub/btcpayapp").ToString(), options =>
{
options.AccessTokenProvider = () => Task.FromResult(_accountManager.GetAccount()?.AccessToken);
})
.WithAutomaticReconnect()
.Build();
_subscription = Connection.Register(_btcPayAppServerClientInterface);
HubProxy = Connection.CreateHubProxy<IBTCPayAppHubServer>();
}
public Task StopAsync(CancellationToken cancellationToken)
{
_authStateProvider.AuthenticationStateChanged -= OnAuthenticationStateChanged;
_btcPayAppServerClient.OnNotifyNetwork += OnNotifyNetwork;
return Task.CompletedTask;
ConnectionChanged -= OnConnectionChanged;
}
public Task OnClosed(Exception? exception)
{
_logger.LogError(exception, "Hub connection closed");
ConnectionState = HubConnectionState.Disconnected;
if (Connection?.State == HubConnectionState.Disconnected)
{
ConnectionState = BTCPayConnectionState.Disconnected;
}
return Task.CompletedTask;
}
public Task OnReconnected(string? connectionId)
public async Task OnReconnected(string? connectionId)
{
_logger.LogInformation("Hub reconnected: {ConnectionId}", connectionId);
ConnectionState = HubConnectionState.Connected;
return Task.CompletedTask;
_logger.LogInformation("Hub connection reconnected");
ConnectionState = BTCPayConnectionState.Syncing;
}
public Task OnReconnecting(Exception? exception)
public async Task OnReconnecting(Exception? exception)
{
_logger.LogWarning(exception, "Hub reconnecting");
ConnectionState = HubConnectionState.Connecting;
return Task.CompletedTask;
_logger.LogWarning(exception, "Hub connection reconnecting");
ConnectionState = BTCPayConnectionState.Connecting;
}
}
}

View File

@ -0,0 +1,13 @@
namespace BTCPayApp.Core.Attempt2;
public enum BTCPayConnectionState
{
Init,
WaitingForAuth,
Connecting,
Syncing,
Disconnected,
ConnectedAsMaster,
ConnectedAsSlave,
ConnectedFinishedInitialSync
}

View File

@ -1,8 +1,7 @@
using BTCPayApp.CommonServer.Models;
using BTCPayApp.Core.Data;
using BTCPayApp.Core.Data;
using BTCPayApp.Core.Helpers;
using BTCPayApp.Core.LDK;
using Microsoft.Extensions.Logging;
using BTCPayServer.Lightning;
namespace BTCPayApp.Core.Attempt2;
@ -26,14 +25,16 @@ public class BTCPayPaymentsNotifier : IScopedHostedService
_paymentsManager.OnPaymentUpdate += OnPaymentUpdate;
}
private async Task OnPaymentUpdate(object? sender, LightningPayment e)
private async Task OnPaymentUpdate(object? sender, AppLightningPayment e)
{
await _connectionManager.HubProxy
.SendPaymentUpdate(
_onChainWalletManager.WalletConfig.Derivations[WalletDerivation.LightningScripts].Identifier, e)
.SendInvoiceUpdate(
_onChainWalletManager.WalletConfig.Derivations[WalletDerivation.LightningScripts].Identifier, e.ToInvoice())
.RunSync();
}
public async Task StopAsync(CancellationToken cancellationToken)
{
_paymentsManager.OnPaymentUpdate -= OnPaymentUpdate;

View File

@ -0,0 +1,32 @@
using BTCPayApp.Core.Contracts;
namespace BTCPayApp.Core.Attempt2;
public static class ConfigHelpers
{
public static async Task<T> GetOrSet<T>(this ISecureConfigProvider secureConfigProvider, string key,
Func<Task<T>> factory)
{
var value = await secureConfigProvider.Get<T>(key);
if (Equals(value, default(T)))
{
value = await factory();
await secureConfigProvider.Set(key, value);
}
return value;
}
public static async Task<T> GetOrSet<T>(this IConfigProvider configProvider, string key, Func<Task<T>> factory,
bool backup)
{
var value = await configProvider.Get<T>(key);
if (Equals(value, default(T)))
{
value = await factory();
await configProvider.Set(key, value, backup);
}
return value;
}
}

View File

@ -13,30 +13,49 @@ public class LDKKVStore:KVStoreInterface
_configProvider = configProvider;
}
private string CombineKey(string primary_namespace, string secondary_namespace, string key)
{
var str = "ln:";
if (!string.IsNullOrEmpty(primary_namespace))
{
str += primary_namespace + ":";
}
if (!string.IsNullOrEmpty(secondary_namespace))
{
str += secondary_namespace + ":";
}
if (!string.IsNullOrEmpty(key))
{
str += key;
}
return str;
}
public Result_CVec_u8ZIOErrorZ read(string primary_namespace, string secondary_namespace, string key)
{
var key1 = $"{primary_namespace}:{secondary_namespace}:{key}";
var key1 = CombineKey(primary_namespace, secondary_namespace, key);
var result = _configProvider.Get<byte[]>(key1).ConfigureAwait(false).GetAwaiter().GetResult();
return result == null ? Result_CVec_u8ZIOErrorZ.err(IOError.LDKIOError_NotFound) : Result_CVec_u8ZIOErrorZ.ok(result);
}
public Result_NoneIOErrorZ write(string primary_namespace, string secondary_namespace, string key, byte[] buf)
{
var key1 = $"{primary_namespace}:{secondary_namespace}:{key}";
_configProvider.Set(key1, buf).ConfigureAwait(false).GetAwaiter().GetResult();
var key1 = CombineKey(primary_namespace, secondary_namespace, key);
_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 = $"{primary_namespace}:{secondary_namespace}:{key}";
_configProvider.Set<byte[]>(key1, null).ConfigureAwait(false).GetAwaiter().GetResult();
var key1 = CombineKey(primary_namespace, secondary_namespace, key);
_configProvider.Set<byte[]>(key1, null, true).ConfigureAwait(false).GetAwaiter().GetResult();
return Result_NoneIOErrorZ.ok();
}
public Result_CVec_StrZIOErrorZ list(string primary_namespace, string secondary_namespace)
{
var key1 = $"{primary_namespace}:{secondary_namespace}:";
var key1 = CombineKey(primary_namespace, secondary_namespace, string.Empty);
var result = _configProvider.List(key1).ConfigureAwait(false).GetAwaiter().GetResult();
return Result_CVec_StrZIOErrorZ.ok(result.ToArray());
}

View File

@ -3,6 +3,7 @@ using BTCPayApp.Core.Contracts;
using BTCPayApp.Core.Data;
using BTCPayApp.Core.Helpers;
using BTCPayApp.Core.LDK;
using BTCPayApp.Core.LSP.JIT;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
@ -98,6 +99,18 @@ public partial class LDKNode:
}
}
public async Task<IJITService?> GetJITLSPService()
{
var config = await GetConfig();
var lsp = config.JITLSP;
if(lsp is null)
{
return null;
}
var jits = ServiceProvider.GetServices<IJITService>();
return jits.FirstOrDefault(jit => jit.ProviderName == lsp);
}
}
public partial class LDKNode : IAsyncDisposable, IHostedService, IDisposable
@ -181,12 +194,17 @@ public partial class LDKNode : IAsyncDisposable, IHostedService, IDisposable
await _configLoaded.Task;
return _config!;
}
private async Task UpdateConfig(LightningConfig config)
public async Task<string[]> GetJITLSPs()
{
return ServiceProvider.GetServices<IJITService>().Select(jit => jit.ProviderName).ToArray();
}
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);
}
@ -222,7 +240,7 @@ public partial class LDKNode : IAsyncDisposable, IHostedService, IDisposable
if (!exists)
return;
var identifier = _onChainWalletManager.WalletConfig.Derivations[WalletDerivation.LightningScripts].Identifier;
// var identifier = _onChainWalletManager.WalletConfig.Derivations[WalletDerivation.LightningScripts].Identifier;
_logger.LogInformation("Stopping LDKNode services");
@ -234,7 +252,7 @@ public partial class LDKNode : IAsyncDisposable, IHostedService, IDisposable
_logger.LogInformation($"Stopped {service.GetType().Name}");
}).ToArray();
await Task.WhenAll(tasks);
_ = _connectionManager.HubProxy.IdentifierActive(identifier, false).RunSync();
// _ = _connectionManager.HubProxy.DeviceMasterSignal(identifier, false).RunSync();
}
@ -273,23 +291,23 @@ public partial class LDKNode : IAsyncDisposable, IHostedService, IDisposable
public async Task<byte[]?> GetRawChannelManager()
{
return await _configProvider.Get<byte[]>("ChannelManager") ?? null;
return await _configProvider.Get<byte[]>("ln:ChannelManager") ?? null;
}
public async Task UpdateChannelManager(ChannelManager serializedChannelManager)
{
await _configProvider.Set("ChannelManager", serializedChannelManager.write());
await _configProvider.Set("ln:ChannelManager", serializedChannelManager.write(), true);
}
public async Task UpdateNetworkGraph(NetworkGraph networkGraph)
{
await _configProvider.Set("NetworkGraph", networkGraph.write());
await _configProvider.Set("ln:NetworkGraph", networkGraph.write(), true);
}
public async Task UpdateScore(WriteableScore score)
{
await _configProvider.Set("Score", score.write());
await _configProvider.Set("ln:Score", score.write(), true);
}
@ -333,42 +351,41 @@ public partial class LDKNode : IAsyncDisposable, IHostedService, IDisposable
}
}
public async Task UpdateChannel(string id, byte[] write)
public async Task UpdateChannel(List<ChannelAlias> identifiers, byte[] write)
{
var ids = identifiers.Select(alias => alias.Id).ToArray();
await using var context = await _dbContextFactory.CreateDbContextAsync();
var channel = await context.LightningChannels.SingleOrDefaultAsync(lightningChannel => lightningChannel.Id == id || lightningChannel.Aliases.Contains(id));
var channel = (await context.ChannelAliases.Include(alias => alias.Channel)
.ThenInclude(channel1 => channel1.Aliases).FirstOrDefaultAsync(alias => ids.Contains(alias.Id)))?.Channel;
if (channel is not null)
{
if (!channel.Aliases.Contains(channel.Id))
foreach (var alias in identifiers)
{
channel.Aliases.Add(channel.Id);
}
if (!channel.Aliases.Contains(id))
{
channel.Aliases.Add(id);
if (channel.Aliases.All(a => a.Id != alias.Id))
{
channel.Aliases.Add(alias);
}
}
channel.Id = id;
channel.Data = write;
}
else
{
await context.LightningChannels.AddAsync(new Channel()
{
Id = id,
Id = identifiers.First().ChannelId,
Data = write,
Aliases = [id]
Aliases = identifiers.ToList()
});
}
await context.SaveChangesAsync();
}
public async Task Peer(string toString, PeerInfo? value)
public async Task Peer(PubKey key, PeerInfo? value)
{
toString = toString.ToLowerInvariant();
var toString = key.ToString().ToLowerInvariant();
var config = await GetConfig();
if (value is null)
{

View File

@ -1,3 +1,4 @@
using BTCPayApp.Core.Auth;
using BTCPayApp.Core.Data;
using BTCPayApp.Core.Helpers;
using Microsoft.AspNetCore.SignalR.Client;
@ -11,6 +12,7 @@ public class LightningNodeManager : BaseHostedService
{
public const string PaymentMethodId = "BTC-LN";
private readonly IAccountManager _accountManager;
private readonly IDbContextFactory<AppDbContext> _dbContextFactory;
private readonly ILogger<LightningNodeManager> _logger;
private readonly OnChainWalletManager _onChainWalletManager;
@ -20,13 +22,12 @@ public class LightningNodeManager : BaseHostedService
private IServiceScope? _nodeScope;
public LDKNode? Node => _nodeScope?.ServiceProvider.GetService<LDKNode>();
private LightningNodeState _state = LightningNodeState.Init;
private bool IsHubConnected => _btcPayConnectionManager.ConnectionState is HubConnectionState.Connected;
private bool IsHubConnected => _btcPayConnectionManager.ConnectionState is BTCPayConnectionState.ConnectedAsMaster;
private bool IsOnchainConfigured => _onChainWalletManager.WalletConfig is not null;
private bool IsOnchainLightningDerivationConfigured => _onChainWalletManager.WalletConfig?.Derivations.ContainsKey(WalletDerivation.LightningScripts) is true;
public bool CanConfigureLightningNode => IsHubConnected && IsOnchainConfigured && !IsOnchainLightningDerivationConfigured && State == LightningNodeState.NotConfigured;
public string? ConnectionString => IsOnchainLightningDerivationConfigured
? $"type=app;group={_onChainWalletManager.WalletConfig!.Derivations[WalletDerivation.LightningScripts].Identifier}".ToLower()
: null;
public string? ConnectionString => IsOnchainLightningDerivationConfigured && _accountManager.GetUserInfo() is {} acc
? $"type=app;user={acc.UserId}": null;
public LightningNodeState State
{
@ -45,12 +46,14 @@ public class LightningNodeManager : BaseHostedService
public event AsyncEventHandler<(LightningNodeState Old, LightningNodeState New)>? StateChanged;
public LightningNodeManager(
IAccountManager accountManager,
IDbContextFactory<AppDbContext> dbContextFactory,
ILogger<LightningNodeManager> logger,
OnChainWalletManager onChainWalletManager,
BTCPayConnectionManager btcPayConnectionManager,
IServiceScopeFactory serviceScopeFactory)
{
_accountManager = accountManager;
_dbContextFactory = dbContextFactory;
_logger = logger;
_onChainWalletManager = onChainWalletManager;
@ -125,9 +128,8 @@ public class LightningNodeManager : BaseHostedService
await _onChainWalletManager.RemoveDerivation(WalletDerivation.LightningScripts);
await using var context = await _dbContextFactory.CreateDbContextAsync();
context.LightningPayments.RemoveRange(context.LightningPayments);
context.LightningChannels.RemoveRange(context.LightningChannels);
context.Settings.RemoveRange(context.Settings.Where(s => new string[]{"ChannelManager","NetworkGraph","Score","lightningconfig"}.Contains(s.Key)));
// context.OutboxItems.RemoveRange(context.OutboxItems);
context.Settings.RemoveRange(context.Settings.Where(s => s.Key.StartsWith("ln:")));
await context.SaveChangesAsync();
}
finally
@ -161,15 +163,16 @@ public class LightningNodeManager : BaseHostedService
}
}
private async Task OnConnectionChanged(object? sender, (HubConnectionState Old, HubConnectionState New) state)
private async Task OnConnectionChanged(object? sender, (BTCPayConnectionState Old, BTCPayConnectionState New) valueTuple)
{
if (IsHubConnected && State == LightningNodeState.WaitingForConnection)
switch (IsHubConnected)
{
State = LightningNodeState.Loading;
}
else if (_btcPayConnectionManager.ConnectionState == HubConnectionState.Disconnected && State is LightningNodeState.Loading or LightningNodeState.Loaded)
{
_ = StopNode();
case true when State == LightningNodeState.WaitingForConnection:
State = LightningNodeState.Loading;
break;
case true when State is LightningNodeState.Loading or LightningNodeState.Loaded:
_ = StopNode();
break;
}
}
@ -206,19 +209,7 @@ public class LightningNodeManager : BaseHostedService
newState = LightningNodeState.NotConfigured;
break;
}
var result = await _btcPayConnectionManager.HubProxy!
.IdentifierActive(_onChainWalletManager.WalletConfig!.Derivations[WalletDerivation.LightningScripts].Identifier, true)
.RunSync();
if (result)
{
await StartNode();
}
else
{
//TODO: Introduce a new state so that this node knows that another instance is active
newState = LightningNodeState.Error;
}
await StartNode();
break;
case LightningNodeState.NotConfigured:

View File

@ -20,7 +20,6 @@ public class OnChainWalletManager : BaseHostedService
private readonly BTCPayAppServerClient _btcPayAppServerClient;
private readonly BTCPayConnectionManager _btcPayConnectionManager;
private readonly ILogger<OnChainWalletManager> _logger;
private readonly IDbContextFactory<AppDbContext> _dbContextFactory;
private readonly IMemoryCache _memoryCache;
private OnChainWalletState _state = OnChainWalletState.Init;
@ -48,14 +47,12 @@ public class OnChainWalletManager : BaseHostedService
BTCPayAppServerClient btcPayAppServerClient,
BTCPayConnectionManager btcPayConnectionManager,
ILogger<OnChainWalletManager> logger,
IDbContextFactory<AppDbContext> dbContextFactory,
IMemoryCache memoryCache)
{
_configProvider = configProvider;
_btcPayAppServerClient = btcPayAppServerClient;
_btcPayConnectionManager = btcPayConnectionManager;
_logger = logger;
_dbContextFactory = dbContextFactory;
_memoryCache = memoryCache;
}
@ -76,7 +73,7 @@ public class OnChainWalletManager : BaseHostedService
}
}
private bool IsHubConnected => _btcPayConnectionManager.ConnectionState is HubConnectionState.Connected;
private bool IsHubConnected => _btcPayConnectionManager.ConnectionState is BTCPayConnectionState.ConnectedAsMaster;
public bool IsConfigured => WalletConfig is not null;
private async Task OnStateChanged(object? sender, (OnChainWalletState Old, OnChainWalletState New) e)
@ -136,7 +133,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;
}
@ -171,7 +168,7 @@ public class OnChainWalletManager : BaseHostedService
Descriptor = descriptor,
Identifier = result[key]
};
await _configProvider.Set(WalletConfig.Key, WalletConfig);
await _configProvider.Set(WalletConfig.Key, WalletConfig, true);
}
finally
{
@ -179,7 +176,7 @@ public class OnChainWalletManager : BaseHostedService
}
}
private async Task ConnectionChanged(object? sender, (HubConnectionState Old, HubConnectionState New) _)
private async Task ConnectionChanged(object? sender, (BTCPayConnectionState Old, BTCPayConnectionState New) valueTuple)
{
DetermineState();
}
@ -438,7 +435,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
{

View File

@ -0,0 +1,59 @@
using System.Security.Cryptography;
using System.Text;
using Microsoft.AspNetCore.DataProtection;
namespace BTCPayApp.Core.Attempt2;
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);
}
}

View File

@ -0,0 +1,289 @@
using System.Net.Http.Headers;
using System.Text.Json;
using BTCPayApp.Core.Auth;
using BTCPayApp.Core.Contracts;
using BTCPayApp.Core.Data;
using BTCPayApp.VSS;
using Google.Protobuf;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using NBitcoin;
using VSSProto;
namespace BTCPayApp.Core.Attempt2;
public class SyncService
{
private readonly ILogger<SyncService> _logger;
private readonly IAccountManager _accountManager;
private readonly IHttpClientFactory _httpClientFactory;
private readonly IDbContextFactory<AppDbContext> _dbContextFactory;
private readonly ISecureConfigProvider _secureConfigProvider;
public SyncService(
ILogger<SyncService> logger,
ISecureConfigProvider secureConfigProvider,
IAccountManager accountManager,
IHttpClientFactory httpClientFactory,
IDbContextFactory<AppDbContext> dbContextFactory)
{
_logger = logger;
_accountManager = accountManager;
_httpClientFactory = httpClientFactory;
_dbContextFactory = dbContextFactory;
_secureConfigProvider = secureConfigProvider;
}
private async Task<IDataProtector> GetDataProtector()
{
var key = await _secureConfigProvider.GetOrSet("encryptionKey",
async () => Convert.ToHexString(RandomUtils.GetBytes(32)).ToLowerInvariant());
return new SingleKeyDataProtector(Convert.FromHexString(key));
}
private async Task<IVSSAPI> GetVSSAPI()
{
var account = _accountManager.GetAccount();
if (account is null)
throw new InvalidOperationException("Account not found");
var vssUri = new Uri(new Uri(account.BaseUri), "vss/");
var httpClient = _httpClientFactory.CreateClient("vss");
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", account.AccessToken);
var vssClient = new HttpVSSAPIClient(vssUri, httpClient);
var protector = await GetDataProtector();
return new VSSApiEncryptorClient(vssClient, protector);
}
private async Task<KeyValue[]> CreateLocalVersions(AppDbContext dbContext)
{
var settings = dbContext.Settings.Where(setting => setting.Backup).Select(setting => new KeyValue()
{
Key = setting.EntityKey,
Version = setting.Version
});
var channels = dbContext.LightningChannels.Select(channel => new KeyValue()
{
Key = channel.EntityKey,
Version = channel.Version
});
var payments = dbContext.LightningPayments.Select(payment => new KeyValue()
{
Key = payment.EntityKey,
Version = payment.Version
});
return await settings.Concat(channels).Concat(payments).ToArrayAsync();
}
public async Task SyncToLocal(CancellationToken cancellationToken = default)
{
var backupApi = await GetVSSAPI();
await using var db = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
var localVersions = await CreateLocalVersions(db);
var remoteVersions = await backupApi.ListKeyVersionsAsync(new ListKeyVersionsRequest(), cancellationToken);
await db.Database.BeginTransactionAsync(cancellationToken);
try
{
var triggers = await db.Database
.SqlQuery<TriggerRecord>($"SELECT name, sql FROM sqlite_master WHERE type = 'trigger'")
.ToListAsync(cancellationToken: cancellationToken);
await db.Database.ExecuteSqlRawAsync(
string.Join("; ", triggers.Select(trigger => $"DROP TRIGGER IF EXISTS {trigger.name}")),
cancellationToken: cancellationToken);
// delete local versions that are not in remote
// delete local versions which are lower than remote
var toDelete = localVersions.Where(localVersion =>
remoteVersions.KeyVersions.All(remoteVersion => remoteVersion.Key != localVersion.Key)
|| remoteVersions.KeyVersions.All(remoteVersion =>
remoteVersion.Key == localVersion.Key && remoteVersion.Version > localVersion.Version)).ToArray();
var toUpsert = remoteVersions.KeyVersions.Where(remoteVersion => localVersions.All(localVersion =>
localVersion.Key != remoteVersion.Key || localVersion.Version < remoteVersion.Version));
foreach (var upsertItem in toUpsert)
{
if (upsertItem.Value is null)
{
var item = await backupApi.GetObjectAsync(new GetObjectRequest()
{
Key = upsertItem.Key,
}, cancellationToken);
upsertItem.MergeFrom(item.Value);
}
}
var settingsToDelete = toDelete.Where(key => key.Key.StartsWith("Setting_")).Select(key => key.Key);
var channelsToDelete = toDelete.Where(key => key.Key.StartsWith("Channel_")).Select(key => key.Key);
var paymentsToDelete = toDelete.Where(key => key.Key.StartsWith("Payment_")).Select(key => key.Key);
await db.Settings.Where(setting => settingsToDelete.Contains(setting.EntityKey))
.ExecuteDeleteAsync(cancellationToken: cancellationToken);
await db.LightningChannels.Where(channel => channelsToDelete.Contains(channel.EntityKey))
.ExecuteDeleteAsync(cancellationToken: cancellationToken);
await db.LightningPayments.Where(payment => paymentsToDelete.Contains(payment.EntityKey))
.ExecuteDeleteAsync(cancellationToken: cancellationToken);
// upsert the rest when needed
var settingsToUpsert = toUpsert.Where(key => key.Key.StartsWith("Setting_")).Select(setting => new Setting()
{
Key = setting.Key.Split('_')[1],
Value = setting.Value.ToByteArray(),
Version = setting.Version,
Backup = true
});
var channelsToUpsert = toUpsert.Where(key => key.Key.StartsWith("Channel_"))
.Select(value => JsonSerializer.Deserialize<Channel>(value.Value.ToStringUtf8())!);
var paymentsToUpsert = toUpsert.Where(key => key.Key.StartsWith("Payment_")).Select(value =>
JsonSerializer.Deserialize<AppLightningPayment>(value.Value.ToStringUtf8())!);
await db.Settings.UpsertRange(settingsToUpsert).On(setting => setting.EntityKey)
.RunAsync(cancellationToken);
await db.LightningChannels.UpsertRange(channelsToUpsert).On(channel => channel.EntityKey)
.RunAsync(cancellationToken);
await db.LightningPayments.UpsertRange(paymentsToUpsert).On(payment => payment.EntityKey)
.RunAsync(cancellationToken);
await db.Database.ExecuteSqlRawAsync(string.Join("; ", triggers.Select(record => record.sql)),
cancellationToken: cancellationToken);
await db.Database.CommitTransactionAsync(cancellationToken);
await db.SaveChangesAsync(cancellationToken);
}
catch (Exception e)
{
await db.Database.RollbackTransactionAsync(cancellationToken);
throw;
}
}
private async Task<KeyValue?> GetValue(AppDbContext dbContext, Outbox outbox)
{
switch (outbox.Entity)
{
case "Setting":
var setting = await dbContext.Settings.SingleOrDefaultAsync(setting1 =>
setting1.EntityKey == outbox.Key && setting1.Backup);
if (setting == null)
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.EntityKey == 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 payment = await dbContext.LightningPayments.SingleOrDefaultAsync(lightningPayment =>
lightningPayment.EntityKey == outbox.Key);
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();
}
}
public async Task SyncToRemote(long deviceIdentifier, CancellationToken cancellationToken = default)
{
var backupAPi = await GetVSSAPI();
await using var db = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
var putObjectRequest = new PutObjectRequest();
var outbox = await db.OutboxItems.GroupBy(outbox1 => new {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.Key, 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
}
putObjectRequest.GlobalVersion = deviceIdentifier;
await backupAPi.PutObjectAsync(putObjectRequest, cancellationToken);
await db.SaveChangesAsync(cancellationToken);
}
private (Task syncTask, CancellationTokenSource cts, bool local)? _syncTask;
public async Task StartSync(bool local, long deviceIdentifier, CancellationToken cancellationToken = default)
{
if (_syncTask.HasValue && _syncTask.Value.local == local)
return;
if (_syncTask.HasValue)
{
await _syncTask.Value.cts.CancelAsync();
}
var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
_syncTask = (ContinuouslySync(deviceIdentifier,local, cts.Token), cts, local);
}
public async Task StopSync()
{
if (_syncTask.HasValue)
{
await _syncTask.Value.cts.CancelAsync();
_syncTask = null;
}
}
private async Task ContinuouslySync(long deviceIdentifier, bool local, CancellationToken cancellationToken = default)
{
while (!cancellationToken.IsCancellationRequested)
{
try
{
if (local)
await SyncToLocal(cancellationToken);
else
await SyncToRemote(deviceIdentifier, cancellationToken);
await Task.Delay(2000, cancellationToken);
}
catch (Exception e)
{
_logger.LogError(e, "Error while syncing to {Local}", local ? "local" : "remote");
}
}
}
}

View File

@ -46,22 +46,26 @@ public class AuthStateProvider : AuthenticationStateProvider, IAccountManager, I
_identityOptions = identityOptions;
}
private CancellationTokenSource? _pingCts;
public async Task StartAsync(CancellationToken cancellationToken)
{
_ = PingOccasionally();
_pingCts = new CancellationTokenSource();
_ = PingOccasionally(_pingCts.Token);
}
private async Task PingOccasionally()
private async Task PingOccasionally(CancellationToken pingCtsToken)
{
while (_userInfo != null)
while (pingCtsToken.IsCancellationRequested is false)
{
await GetAuthenticationStateAsync();
await Task.Delay(TimeSpan.FromSeconds(5));
await Task.Delay(TimeSpan.FromSeconds(5), pingCtsToken);
}
}
public Task StopAsync(CancellationToken cancellationToken)
{
_pingCts?.Cancel();
return Task.CompletedTask;
}
@ -412,12 +416,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, false);
}
public async Task RemoveAccount(BTCPayAccount account)
{
await _config.Set<BTCPayAccount>(GetKey(account.Id), null);
await _config.Set<BTCPayAccount>(GetKey(account.Id), null, false);
}
private async Task<BTCPayAccount> GetAccount(string serverUrl, string email)
@ -438,7 +442,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, false);
_account = account;
_userInfo = null;

View File

@ -12,8 +12,12 @@
<ItemGroup>
<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" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.6" />
@ -38,6 +42,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BTCPayApp.VSS\BTCPayApp.VSS.csproj" />
<ProjectReference Include="..\submodules\btcpayserver\BTCPayApp.CommonServer\BTCPayApp.CommonServer.csproj" />
<ProjectReference Include="..\submodules\btcpayserver\BTCPayServer.Abstractions\BTCPayServer.Abstractions.csproj" />
<ProjectReference Include="..\submodules\btcpayserver\BTCPayServer.Client\BTCPayServer.Client.csproj" />

View File

@ -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);
}

View File

@ -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);
}

View File

@ -1,7 +1,14 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json;
using System.ComponentModel.DataAnnotations.Schema;
using BTCPayApp.CommonServer.Models;
using BTCPayApp.Core.JsonConverters;
using BTCPayApp.Core.LDK;
using BTCPayServer.Lightning;
using Laraue.EfCoreTriggers.Common.Extensions;
using Laraue.EfCoreTriggers.Common.TriggerBuilders.Actions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Hosting;
using NBitcoin;
namespace BTCPayApp.Core.Data;
@ -14,27 +21,184 @@ public class AppDbContext : DbContext
public DbSet<Setting> Settings { get; set; }
public DbSet<Channel> LightningChannels { get; set; }
public DbSet<LightningPayment> LightningPayments { get; set; }
// public DbSet<SpendableCoin> SpendableCoins { get; set; }
public DbSet<ChannelAlias> ChannelAliases { get; set; }
public DbSet<AppLightningPayment> LightningPayments { get; set; }
public DbSet<Outbox> OutboxItems { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
//TODO: add paymentId to the primary key and generate a random one if not provided
modelBuilder.Entity<LightningPayment>()
.HasKey(w => new {w.PaymentHash, w.Inbound, w.PaymentId});
//we use system.text.json because it is natively supported in efcore for querying
modelBuilder.Entity<LightningPayment>().Property(p => p.AdditionalData)
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(
v => JsonSerializer.Serialize(v, JsonSerializerOptions.Default),
v => JsonSerializer.Deserialize<Dictionary<string, JsonDocument>>(v, JsonSerializerOptions.Default)!);
request => request.ToString(),
str => NetworkHelper.Try(network => BOLT11PaymentRequest.Parse(str, network)));
modelBuilder.Entity<AppLightningPayment>().Property(payment => payment.Secret)
.HasConversion(
request => request.ToString(),
str => uint256.Parse(str));
modelBuilder.Entity<AppLightningPayment>().Property(payment => payment.PaymentHash)
.HasConversion(
request => request.ToString(),
str => uint256.Parse(str));
modelBuilder.Entity<AppLightningPayment>().Property(payment => payment.Value)
.HasConversion(
request => request.MilliSatoshi,
str => new LightMoney(str));
modelBuilder.Entity<AppLightningPayment>().Property(payment => payment.AdditionalData).HasJsonConversion();
modelBuilder.Entity<AppLightningPayment>()
.HasKey(w => new {w.PaymentHash, w.Inbound, w.PaymentId});
//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
// when deleting, insert an outbox item
modelBuilder.Entity<Setting>()
.AfterInsert(trigger => trigger
.Action(group =>
{
group
.Condition(@ref => @ref.New.Backup)
.Insert(
// .InsertIfNotExists( (@ref, outbox) => outbox.Version == @ref.New.Version && outbox.ActionType == OutboxAction.Insert && outbox.Entity == "Setting" && outbox.Key == @ref.New.Key,
@ref => new Outbox()
{
Entity = "Setting",
Version = @ref.New.Version,
Key = @ref.New.EntityKey,
ActionType = OutboxAction.Insert
});
}))
.AfterDelete(trigger => trigger
.Action(group => group
.Condition(@ref => @ref.Old.Backup)
.Insert(
// .InsertIfNotExists( (@ref, outbox) => @ref.Old.Version == outbox.Version && outbox.ActionType == OutboxAction.Delete && outbox.Entity == "Setting" && outbox.Key == @ref.Old.Key,
@ref => new Outbox()
{
Entity = "Setting",
Version = @ref.Old.Version,
Key = @ref.Old.EntityKey,
ActionType = OutboxAction.Delete
})))
.AfterUpdate(trigger => trigger
.Action(group => group
.Condition(@ref => @ref.Old.Backup)
// .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.EntityKey,
ActionType = OutboxAction.Update
})));
// .Action(group => group
// .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,
// @ref => new Outbox()
// {
// Entity = "Setting",
// Version = @ref.Old.Version +1,
// Key = @ref.New.Key,
// 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.EntityKey,
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.EntityKey,
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.EntityKey,
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.EntityKey,
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.EntityKey,
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.EntityKey,
ActionType = OutboxAction.Update
})));
base.OnModelCreating(modelBuilder);
}
}
public class SpendableCoin
{
public string Script { get; set; }
[Key] public string Outpoint { get; set; }
public byte[] Data { get; set; }
}

View File

@ -0,0 +1,41 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using BTCPayApp.Core.JsonConverters;
using BTCPayServer.Lightning;
using NBitcoin;
namespace BTCPayApp.Core.Data;
public class AppLightningPayment : VersionedData
{
[JsonConverter(typeof(UInt256JsonConverter))]
public uint256 PaymentHash { get; set; }
public string PaymentId { get; set; }
public string? Preimage { get; set; }
[JsonConverter(typeof(UInt256JsonConverter))]
public uint256 Secret { get; set; }
public bool Inbound { get; set; }
[JsonConverter(typeof(DateTimeToUnixTimeConverter))]
public DateTimeOffset Timestamp { get; set; }
[JsonConverter(typeof(LightMoneyJsonConverter))]
public LightMoney Value { get; set; }
[JsonConverter(typeof(JsonStringEnumConverter))]
public LightningPaymentStatus Status { get; set; }
[JsonConverter(typeof(BOLT11PaymentRequestJsonConverter))]
public BOLT11PaymentRequest PaymentRequest { get; set; }
[JsonExtensionData] public Dictionary<string, JsonElement> AdditionalData { get; set; } = new();
public override string EntityKey
{
get => $"Payment_{PaymentHash}_{PaymentId}_{Inbound}";
init { }
}
}

View File

@ -1,10 +1,27 @@
namespace BTCPayApp.Core.Data;
using System.Text.Json.Serialization;
public class Channel
namespace BTCPayApp.Core.Data;
public class Channel:VersionedData
{
public string Id { get; set; }
public List<string> Aliases { get; set; }
public byte[] Data { get; set; }
public List<ChannelAlias> Aliases { get; set; }
public override string EntityKey
{
get => $"Channel_{Id}";
init { }
}
}
public class ChannelAlias
{
public string Id { get; set; }
public string Type { get; set; }
public string ChannelId { get; set; }
[JsonIgnore]
public Channel Channel { get; set; }
}

View File

@ -1,4 +1,5 @@
using Microsoft.EntityFrameworkCore;
using Laraue.EfCoreTriggers.SqlLite.Extensions;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
namespace BTCPayApp.Core.Data;
@ -9,6 +10,7 @@ public class DesignTimeAppContextFactory : IDesignTimeDbContextFactory<AppDbCont
{
var optionsBuilder = new DbContextOptionsBuilder<AppDbContext>();
optionsBuilder.UseSqlite("Data Source=fake.db");
optionsBuilder.UseSqlLiteTriggers();
return new AppDbContext(optionsBuilder.Options);
}

View File

@ -0,0 +1,23 @@
using Microsoft.EntityFrameworkCore;
namespace BTCPayApp.Core.Data;
public static class EFExtensions
{
public static async Task<int> Upsert<T>(this DbContext ctx, T item, CancellationToken cancellationToken) where T : class
{
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);
// }
}
}

View File

@ -3,7 +3,7 @@
namespace BTCPayApp.Core.Data;
public class LightningConfig
{
public const string Key = "lightningconfig";
public const string Key = "ln:lightningconfig";
public string Alias { get; set; } = "BTCPay Server";
public string ScriptDerivationKey { get; set; } = WalletDerivation.NativeSegwit; //when ldk asks for an address, where do we get it from?
@ -42,11 +42,4 @@ public class LightningConfig
public Dictionary<string, PeerInfo> Peers { get; set; } = new();
public bool AcceptInboundConnection{ get; set; }
}
public record PeerInfo
{
public string Endpoint { get; set; }
public bool Persistent { get; set; }
public bool Trusted { get; set; }
}

View File

@ -0,0 +1,10 @@
namespace BTCPayApp.Core.Data;
public class Outbox
{
public DateTimeOffset Timestamp { get; set; } = DateTimeOffset.Now;
public OutboxAction ActionType { get; set; }
public string Key { get; set; }
public string Entity { get; set; }
public long Version { get; set; }
}

View File

@ -0,0 +1,8 @@
namespace BTCPayApp.Core.Data;
public enum OutboxAction
{
Insert,
Update,
Delete
}

View File

@ -0,0 +1,8 @@
namespace BTCPayApp.Core.Data;
public record PeerInfo
{
public string Endpoint { get; set; }
public bool Persistent { get; set; }
public bool Trusted { get; set; }
}

View File

@ -0,0 +1,30 @@
using System.Text.Json;
using BTCPayApp.Core.Attempt2;
using Microsoft.EntityFrameworkCore;
using VSSProto;
namespace BTCPayApp.Core.Data;
class TriggerRecord
{
public string name { get; set; }
public string sql { get; set; }
}
public class RemoteToLocalSyncService
{
private readonly IDbContextFactory<AppDbContext> _dbContextFactory;
private readonly BTCPayConnectionManager _btcPayConnectionManager;
public RemoteToLocalSyncService(IDbContextFactory<AppDbContext> dbContextFactory,
BTCPayConnectionManager btcPayConnectionManager)
{
_dbContextFactory = dbContextFactory;
_btcPayConnectionManager = btcPayConnectionManager;
}
// on connected to btcpay, sync all the data from the remote to the local
// if we are the active node
}

View File

@ -2,9 +2,16 @@
namespace BTCPayApp.Core.Data;
public class Setting
public class Setting:VersionedData
{
[Key]
public string Key { get; set; }
public byte[] Value { get; set; }
}
public bool Backup { get; set; } = true;
public override string EntityKey
{
get => $"Setting_{Key}";
init { }
}
}

View 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;
}
}

View File

@ -0,0 +1,36 @@
using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace BTCPayApp.Core.Data;
public static class ValueConversionExtensions
{
public static PropertyBuilder<T> HasJsonConversion<T>(this PropertyBuilder<T> propertyBuilder)
where T : class, new()
{
var converter = new ValueConverter<T, string>
(
v => JsonSerializer.Serialize(v, JsonSerializerOptions.Default),
v => JsonSerializer.Deserialize<T>(v, JsonSerializerOptions.Default) ?? new T()
);
var comparer = new ValueComparer<T>
(
(l, r) => JsonSerializer.Serialize(l, JsonSerializerOptions.Default) ==
JsonSerializer.Serialize(r, JsonSerializerOptions.Default),
v => v == null ? 0 : JsonSerializer.Serialize(v, JsonSerializerOptions.Default).GetHashCode(),
v => JsonSerializer.Deserialize<T>(JsonSerializer.Serialize(v, JsonSerializerOptions.Default),
JsonSerializerOptions.Default)!
);
propertyBuilder.HasConversion(converter);
propertyBuilder.Metadata.SetValueConverter(converter);
propertyBuilder.Metadata.SetValueComparer(comparer);
propertyBuilder.HasColumnType("jsonb");
return propertyBuilder;
}
}

View File

@ -0,0 +1,11 @@
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json;
namespace BTCPayApp.Core.Data;
public abstract class VersionedData
{
public long Version { get; set; } = 0;
public abstract string EntityKey { get; init; }
}

View File

@ -1,17 +1,104 @@
using System.Text.Json;
// using System.Collections.Concurrent;
// using System.Text.Json;
// using BTCPayApp.Core.Attempt2;
// using BTCPayApp.Core.Contracts;
// using BTCPayApp.Core.Data;
// using BTCPayApp.VSS;
// using BTCPayServer.Lightning;
// using Microsoft.EntityFrameworkCore;
// using Microsoft.EntityFrameworkCore.Diagnostics;
// using Microsoft.Extensions.Logging;
// using VSSProto;
//
// namespace BTCPayApp.Core;
//
//
//
// public class VSSMapperInterceptor : SaveChangesInterceptor
// {
//
// public VSSMapperInterceptor(BTCPayConnectionManager btcPayConnectionManager, ILogger<VSSMapperInterceptor> logger)
// {
// }
//
// private ConcurrentDictionary<EventId, object> PendingEvents = new ConcurrentDictionary<EventId, object>();
// public override ValueTask<int> SavedChangesAsync(SaveChangesCompletedEventData eventData, int result,
// CancellationToken cancellationToken = new CancellationToken())
// {
// return base.SavedChangesAsync(eventData, result, cancellationToken);
// }
//
// private IVSSAPI api;
// public override ValueTask<InterceptionResult<int>> SavingChangesAsync(DbContextEventData eventData, InterceptionResult<int> result,
// CancellationToken cancellationToken = new CancellationToken())
// {
// foreach (var entry in eventData.Context.ChangeTracker.Entries())
// {
//
// if (entry.Entity is LightningPayment lightningPayment)
// {
// if (entry.State == EntityState.Deleted)
// {
//
// api.DeleteObjectAsync(new DeleteObjectRequest
// {
// KeyValue = new KeyValue()
// {
//
// }
// Key = $"LightningPayment/{lightningPayment.Id}"
// });
// }
// }
// if (entry.Entity is Channel channel)
// {
//
// }
// if (entry.Entity is Setting setting)
// {
//
// }
// }
//
// return base.SavingChangesAsync(eventData, result, cancellationToken);
// }
//
// public override Task SaveChangesCanceledAsync(DbContextEventData eventData,
// CancellationToken cancellationToken = new CancellationToken())
// {
// PendingEvents.Remove(eventData.EventId, out _);
// return base.SaveChangesCanceledAsync(eventData, cancellationToken);
// }
//
// public override Task SaveChangesFailedAsync(DbContextErrorEventData eventData,
// CancellationToken cancellationToken = new CancellationToken())
// {
// PendingEvents.Remove(eventData.EventId, out _);
// return base.SaveChangesFailedAsync(eventData, cancellationToken);
// }
//
//
// }
//
using System.Text.Json;
using AsyncKeyedLock;
using BTCPayApp.Core.Contracts;
using BTCPayApp.Core.Data;
using Microsoft.EntityFrameworkCore;
namespace BTCPayApp.Core;
using Microsoft.Extensions.Logging;
public class DatabaseConfigProvider: IConfigProvider
{
private readonly IDbContextFactory<AppDbContext> _dbContextFactory;
private readonly ILogger<DatabaseConfigProvider> _logger;
private AsyncKeyedLocker<string> _lock = new();
public DatabaseConfigProvider(IDbContextFactory<AppDbContext> dbContextFactory)
public DatabaseConfigProvider(IDbContextFactory<AppDbContext> dbContextFactory, ILogger<DatabaseConfigProvider> logger)
{
_dbContextFactory = dbContextFactory;
_logger = logger;
}
public async Task<T?> Get<T>(string key)
@ -23,8 +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} {backup}", key, value, backup? "backup": "no backup");
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
if (value is null)
{
@ -40,7 +129,8 @@ public class DatabaseConfigProvider: IConfigProvider
}
var newValue = typeof(T) == typeof(byte[])? value as byte[]:JsonSerializer.SerializeToUtf8Bytes(value);
await dbContext.Upsert(new Setting {Key = key, Value = newValue}).RunAsync();
var setting = new Setting {Key = key, Value = newValue, Backup = backup};
await dbContext.Upsert(setting, CancellationToken.None);
}

View File

@ -23,17 +23,17 @@ public static class ChannelExtensions
await channel.Writer.WriteAsync(evt, cancellationToken);
}
add(new AsyncEventHandler<TEvent>(OnEvent));
_ = ProcessChannel(channel, processor, cancellationToken);
add(OnEvent);
_ = channel.ProcessChannel(processor, cancellationToken);
return new DisposableWrapper(async () =>
{
remove(new AsyncEventHandler<TEvent>(OnEvent));
remove(OnEvent);
channel.Writer.Complete();
});
}
private static async Task ProcessChannel<TEvent>(Channel<TEvent> channel, Func<TEvent, CancellationToken, Task> processor, CancellationToken cancellationToken)
public static async Task ProcessChannel<TEvent>(this Channel<TEvent> channel, Func<TEvent, CancellationToken, Task> processor, CancellationToken cancellationToken)
{
while (await channel.Reader.WaitToReadAsync(cancellationToken))
{

View File

@ -0,0 +1,116 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using BTCPayServer.Lightning;
using NBitcoin;
namespace BTCPayApp.Core.JsonConverters;
public class DateTimeToUnixTimeConverter : JsonConverter<DateTimeOffset>
{
public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
switch (reader.TokenType)
{
case JsonTokenType.Null:
return default;
case JsonTokenType.Number:
return DateTimeOffset.FromUnixTimeSeconds(reader.GetInt64());
case JsonTokenType.String:
return DateTimeOffset.FromUnixTimeSeconds(long.Parse(reader.GetString()));
}
throw new JsonException("Expected number or string with a unix timestamp value");
}
public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options)
{
writer.WriteNumberValue(value.ToUnixTimeSeconds());
}
}
public class LightMoneyJsonConverter : JsonConverter<LightMoney>
{
public override LightMoney? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return reader.TokenType switch
{
JsonTokenType.String => LightMoney.Parse(reader.GetString()),
JsonTokenType.Null => null,
_ => throw new ArgumentOutOfRangeException()
};
}
public override void Write(Utf8JsonWriter writer, LightMoney value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString());
}
}
public class UInt256JsonConverter : JsonConverter<uint256>
{
public override uint256? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null)
{
return null;
}
if (reader.TokenType != JsonTokenType.String)
{
throw new JsonException("Expected string");
}
return uint256.Parse(reader.GetString());
}
public override void Write(Utf8JsonWriter writer, uint256 value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString());
}
}
public static class NetworkHelper
{
public static T Try<T>(Func<Network, T> func)
{
Exception? lastException = null;
foreach (var network in Network.GetNetworks())
{
try
{
return func.Invoke(network);
}
catch (Exception e)
{
lastException = e;
}
}
throw lastException!;
}
}
public class BOLT11PaymentRequestJsonConverter : JsonConverter<BOLT11PaymentRequest>
{
public override BOLT11PaymentRequest? Read(ref Utf8JsonReader reader, Type typeToConvert,
JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null)
{
return null;
}
if (reader.TokenType != JsonTokenType.String)
{
throw new JsonException("Expected string");
}
var str = reader.GetString();
return NetworkHelper.Try(network => BOLT11PaymentRequest.Parse(str, network));
}
public override void Write(Utf8JsonWriter writer, BOLT11PaymentRequest value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString());
}
}

View File

@ -2,7 +2,9 @@
using System.Net.Sockets;
using BTCPayApp.Core.Attempt2;
using BTCPayApp.Core.Contracts;
using BTCPayApp.Core.Data;
using BTCPayApp.Core.Helpers;
using BTCPayApp.Core.LSP.JIT;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using NBitcoin;
@ -213,6 +215,7 @@ public static class LDKExtensions
// services.AddScoped<IScopedHostedService>(provider =>
// provider.GetRequiredService<LDKSpendableOutputEventHandler>());
services.AddScoped<IScopedHostedService>(provider => provider.GetRequiredService<LDKChannelSync>());
services.AddScoped<IScopedHostedService>(provider => provider.GetRequiredService<PaymentsManager>());
services.AddScoped<IScopedHostedService>(provider => provider.GetRequiredService<LDKBackgroundProcessor>());
services.AddScoped<IScopedHostedService>(provider => provider.GetRequiredService<LDKPeerHandler>());
services.AddScoped<IScopedHostedService>(provider => provider.GetRequiredService<LDKAnnouncementBroadcaster>());
@ -300,6 +303,11 @@ public static class LDKExtensions
ProbabilisticScoringFeeParameters.with_default()));
services.AddScoped<Router>(provider => provider.GetRequiredService<DefaultRouter>().as_Router());
services.AddScoped<VoltageFlow2Jit>();
services.AddScoped<IScopedHostedService>(provider => provider.GetRequiredService<VoltageFlow2Jit>());
services.AddScoped<IJITService, VoltageFlow2Jit>(provider => provider.GetRequiredService<VoltageFlow2Jit>());
return services;
}

View File

@ -1,4 +1,5 @@
using BTCPayApp.Core.Attempt2;
using BTCPayApp.Core.Helpers;
using org.ldk.structs;
using UInt128 = org.ldk.util.UInt128;
@ -33,6 +34,7 @@ public class LDKOpenChannelRequestEventHandler: ILDKEventHandler<Event.Event_Ope
eventOpenChannelRequest.counterparty_node_id,
userChannelId
);
AcceptedChannel?.Invoke(this, eventOpenChannelRequest);
return;
}
}
@ -43,8 +45,11 @@ public class LDKOpenChannelRequestEventHandler: ILDKEventHandler<Event.Event_Ope
eventOpenChannelRequest.counterparty_node_id,
userChannelId);
AcceptedChannel?.Invoke(this, eventOpenChannelRequest);
//TODO: if we want to reject the channel, we can call reject_channel
//_channelManager.force_close_without_broadcasting_txn(eventOpenChannelRequest.temporary_channel_id, eventOpenChannelRequest.counterparty_node_id);
}
public AsyncEventHandler<Event.Event_OpenChannelRequest>? AcceptedChannel;
}

View File

@ -65,7 +65,7 @@ public class LDKPeerHandler : IScopedHostedService
if (config.Peers.ContainsKey(nodeInfo.NodeId.ToString()))
return;
var endpoint = new IPEndPoint(IPAddress.Parse(nodeInfo.Host), nodeInfo.Port);
await _node.Peer(nodeInfo.NodeId.ToString(), new PeerInfo()
await _node.Peer(nodeInfo.NodeId, new PeerInfo()
{
Endpoint = endpoint.ToString(),
Persistent = true,
@ -224,7 +224,7 @@ public class LDKPeerHandler : IScopedHostedService
if (peer.Endpoint != remote.ToString())
{
peer.Endpoint = remote.ToString()!;
await _node.Peer(theirNodeId.ToString(), peer);
await _node.Peer(theirNodeId, peer);
}
}

View File

@ -1,5 +1,6 @@
using System.Collections.Concurrent;
using BTCPayApp.Core.Attempt2;
using BTCPayApp.Core.Data;
using BTCPayApp.Core.Helpers;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
@ -28,6 +29,8 @@ public class LDKPersistInterface : PersistInterface//, IScopedHostedService
private ConcurrentDictionary<long, Task> updateTasks = new();
public ChannelMonitorUpdateStatus persist_new_channel(OutPoint channel_funding_outpoint, ChannelMonitor data,
MonitorUpdateId update_id)
{
@ -44,9 +47,32 @@ public class LDKPersistInterface : PersistInterface//, IScopedHostedService
var outs = data.get_outputs_to_watch()
.SelectMany(zzzz => zzzz.get_b().Select(zz => Script.FromBytesUnsafe(zz.get_b()))).ToArray();
var id = Convert.ToHexString(ChannelId.v1_from_funding_outpoint(channel_funding_outpoint).get_a()).ToLower();
var fundingId = Convert.ToHexString(ChannelId.v1_from_funding_outpoint(channel_funding_outpoint).get_a()).ToLower();
var identifiers = new List<ChannelAlias>();
identifiers.Add(new ChannelAlias()
{
Id = fundingId,
Type = "funding_outpoint"
});
var otherId = data.channel_id().is_zero()? null: Convert.ToHexString(data.channel_id().get_a()).ToLower();
if(otherId == fundingId)
{
otherId = null;
}
if(otherId != null)
{
identifiers.Add(new ChannelAlias()
{
Id = otherId,
Type = "arbitrary_id"
});
}
// var trackTask = _node.TrackScripts(outs).ContinueWith(task => _logger.LogDebug($"Tracking scripts finished for updateid: {update_id.hash()}"));;
var updateTask = _node.UpdateChannel(id, data.write()).ContinueWith(task => _logger.LogDebug($"Updating channel finished for updateid: {update_id.hash()}"));;
var updateTask = _node.UpdateChannel(identifiers, data.write()).ContinueWith(task => _logger.LogDebug($"Updating channel finished for updateid: {update_id.hash()}"));;
await updateTask;
await Task.Run(() =>
@ -108,9 +134,31 @@ public class LDKPersistInterface : PersistInterface//, IScopedHostedService
var taskResult = updateTasks.GetOrAdd(updateId, async l =>
{
await _node.UpdateChannel(
Convert.ToHexString(ChannelId.v1_from_funding_outpoint(channel_funding_outpoint).get_a()).ToLower(),
data.write());
var fundingId = Convert.ToHexString(ChannelId.v1_from_funding_outpoint(channel_funding_outpoint).get_a()).ToLower();
var identifiers = new List<ChannelAlias>();
identifiers.Add(new ChannelAlias()
{
Id = fundingId,
Type = "funding_outpoint"
});
var otherId = data.channel_id().is_zero()? null: Convert.ToHexString(data.channel_id().get_a()).ToLower();
if(otherId == fundingId)
{
otherId = null;
}
if(otherId != null)
{
identifiers.Add(new ChannelAlias()
{
Id = otherId,
Type = "arbitrary_id"
});
}
await _node.UpdateChannel(identifiers, data.write());
await Task.Run(() =>
{

View File

@ -1,45 +1,56 @@
using System.Security.Cryptography;
using System.Collections.Concurrent;
using System.Text.Json;
using BTCPayApp.Core;
using BTCPayApp.Core.Attempt2;
using BTCPayApp.Core.Data;
using BTCPayApp.Core.Helpers;
using BTCPayApp.Core.LDK;
using BTCPayApp.Core.LSP.JIT;
using BTCPayServer.Lightning;
using Microsoft.EntityFrameworkCore;
using NBitcoin;
using org.ldk.structs;
// using BTCPayServer.Lightning;
using LightningPayment = BTCPayApp.CommonServer.Models.LightningPayment;
namespace BTCPayApp.Core.LDK;
public class PaymentsManager :
IScopedHostedService,
ILDKEventHandler<Event.Event_PaymentClaimable>,
ILDKEventHandler<Event.Event_PaymentClaimed>,
ILDKEventHandler<Event.Event_PaymentFailed>,
ILDKEventHandler<Event.Event_PaymentSent>
{
public const string LightningPaymentDescriptionKey = "DescriptionHash";
public const string LightningPaymentExpiryKey = "Expiry";
private readonly IDbContextFactory<AppDbContext> _dbContextFactory;
private readonly ChannelManager _channelManager;
private readonly LDKOpenChannelRequestEventHandler _openChannelRequestEventHandler;
private readonly Logger _logger;
private readonly NodeSigner _nodeSigner;
private readonly Network _network;
private readonly LDKNode _ldkNode;
public PaymentsManager(
IDbContextFactory<AppDbContext> dbContextFactory,
ChannelManager channelManager,
LDKOpenChannelRequestEventHandler openChannelRequestEventHandler,
Logger logger,
NodeSigner nodeSigner,
Network network
)
Network network,
LDKNode ldkNode)
{
_dbContextFactory = dbContextFactory;
_channelManager = channelManager;
_openChannelRequestEventHandler = openChannelRequestEventHandler;
_logger = logger;
_nodeSigner = nodeSigner;
_network = network;
_ldkNode = ldkNode;
}
public async Task<List<LightningPayment>> List(
Func<IQueryable<LightningPayment>, IQueryable<LightningPayment?>> filter,
public async Task<List<AppLightningPayment>> List(
Func<IQueryable<AppLightningPayment>, IQueryable<AppLightningPayment?>> filter,
CancellationToken cancellationToken = default)
{
await using var context = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
@ -47,62 +58,107 @@ public class PaymentsManager :
.ToListAsync(cancellationToken: cancellationToken))!;
}
public async Task<LightningPayment> RequestPayment(LightMoney amount, TimeSpan expiry, uint256 descriptionHash)
// private Bolt11Invoice CreateInvoice(long? amt, int expirySeconds, byte[] descHash)
// {
// var keyMaterial = _nodeSigner.get_inbound_payment_key_material();
// var preimage = RandomUtils.GetBytes(32);
// var paymentHash = SHA256.HashData(preimage);
// var expandedKey = ExpandedKey.of(keyMaterial);
// var inboundPayment = _channelManager.create_inbound_payment_for_hash(paymentHash,
// amt is null ? Option_u64Z.none() : Option_u64Z.some(amt.Value), expirySeconds, Option_u16Z.none()));
// var paymentSecret = inboundPayment is Result_ThirtyTwoBytesNoneZ.Result_ThirtyTwoBytesNoneZ_OK ok
// ? ok.res
// : throw new Exception("Error creating inbound payment");
//
// _nodeSigner.
// var invoice = Bolt11Invoice.from_signed(_channelManager, _nodeSigner, _logger, _network.GetLdkCurrency(),
// }
public async Task<AppLightningPayment> RequestPayment(LightMoney amount, TimeSpan expiry, uint256 descriptionHash)
{
var amt = amount == LightMoney.Zero ? Option_u64Z.none() : Option_u64Z.some(amount.MilliSatoshi);
var epoch = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
// var result =
// org.ldk.util.UtilMethods.create_invoice_from_channelmanager_and_duration_since_epoch_with_payment_hash(
// _channelManager, _nodeSigner, _logger,
// _network.GetLdkCurrency(), amt, description, epoch, (int) Math.Ceiling(expiry.TotalSeconds),
// paymentHash, Option_u16Z.none());
var now = DateTimeOffset.UtcNow;
var epoch = now.ToUnixTimeSeconds();
var descHashBytes = Sha256.from_bytes(descriptionHash.ToBytes());
var lsp = await _ldkNode.GetJITLSPService();
generateInvoice:
JITFeeResponse? jitFeeReponse = null;
if (lsp is not null)
{
jitFeeReponse = await lsp.CalculateInvoiceAmount(amount);
if (jitFeeReponse is not null)
{
amt = Option_u64Z.some(jitFeeReponse.AmountToGenerateOurInvoice);
}
else
{
lsp = null;
}
}
var result = await Task.Run(() =>
org.ldk.util.UtilMethods.create_invoice_from_channelmanager_with_description_hash_and_duration_since_epoch(
_channelManager, _nodeSigner, _logger,
_network.GetLdkCurrency(), amt, descHashBytes, epoch, (int) Math.Ceiling(expiry.TotalSeconds),
Option_u16Z.none()));
if (result is Result_Bolt11InvoiceSignOrCreationErrorZ.Result_Bolt11InvoiceSignOrCreationErrorZ_Err err)
{
throw new Exception(err.err.to_str());
}
var originalInvoice = ((Result_Bolt11InvoiceSignOrCreationErrorZ.Result_Bolt11InvoiceSignOrCreationErrorZ_OK) result)
.res;
var preimageResult = _channelManager.get_payment_preimage(originalInvoice.payment_hash(), originalInvoice.payment_secret());
var preimage = preimageResult switch
{
Result_ThirtyTwoBytesAPIErrorZ.Result_ThirtyTwoBytesAPIErrorZ_Err errx => throw new Exception(
errx.err.GetError()),
Result_ThirtyTwoBytesAPIErrorZ.Result_ThirtyTwoBytesAPIErrorZ_OK ok => ok.res,
_ => throw new Exception("Unknown error retrieving preimage")
};
var result = await Task.Run(() =>
org.ldk.util.UtilMethods.create_invoice_from_channelmanager_with_description_hash_and_duration_since_epoch(
_channelManager, _nodeSigner, _logger,
_network.GetLdkCurrency(), amt, descHashBytes, epoch, (int) Math.Ceiling(expiry.TotalSeconds),
Option_u16Z.none()));
if (result is Result_Bolt11InvoiceSignOrCreationErrorZ.Result_Bolt11InvoiceSignOrCreationErrorZ_Err err)
{
throw new Exception(err.err.to_str());
}
var invoice = ((Result_Bolt11InvoiceSignOrCreationErrorZ.Result_Bolt11InvoiceSignOrCreationErrorZ_OK) result)
.res;
var preimageResult = _channelManager.get_payment_preimage(invoice.payment_hash(), invoice.payment_secret());
byte[] preimage = null;
if (preimageResult is
Result_ThirtyTwoBytesAPIErrorZ.Result_ThirtyTwoBytesAPIErrorZ_Err errx)
{
throw new Exception(errx.err.GetError());
}else if (preimageResult is Result_ThirtyTwoBytesAPIErrorZ.Result_ThirtyTwoBytesAPIErrorZ_OK ok)
{
preimage = ok.res;
}
var bolt11 = invoice.to_str();
var lp = new LightningPayment()
var parsedOriginalInvoice= BOLT11PaymentRequest.Parse(originalInvoice.to_str(), _network);
var lp = new AppLightningPayment()
{
Inbound = true,
PaymentId = "default",
Value = amount.MilliSatoshi,
PaymentHash = Convert.ToHexString(invoice.payment_hash()).ToLower(),
Secret = Convert.ToHexString(invoice.payment_secret()).ToLower(),
PaymentHash = parsedOriginalInvoice.PaymentHash!,
Secret = parsedOriginalInvoice.PaymentSecret!,
Preimage = Convert.ToHexString(preimage!).ToLower(),
Status = LightningPaymentStatus.Pending,
Timestamp = DateTimeOffset.FromUnixTimeSeconds(epoch),
PaymentRequests = [bolt11]
Timestamp = now,
PaymentRequest = parsedOriginalInvoice,
AdditionalData = new Dictionary<string, JsonElement>()
{
[LightningPaymentDescriptionKey] = JsonSerializer.SerializeToElement(descriptionHash.ToString()),
[LightningPaymentExpiryKey] = JsonSerializer.SerializeToElement(now.Add(expiry))
}
};
if (lsp is not null)
{
if(!await lsp.WrapInvoice(lp,jitFeeReponse ))
{
amt = amount == LightMoney.Zero ? Option_u64Z.none() : Option_u64Z.some(amount.MilliSatoshi);
lsp = null;
goto generateInvoice;
}
}
await Payment(lp);
return lp;
}
public async Task<LightningPayment> PayInvoice(BOLT11PaymentRequest paymentRequest,
public async Task<AppLightningPayment> PayInvoice(BOLT11PaymentRequest paymentRequest,
LightMoney? explicitAmount = null)
{
var id = RandomUtils.GetBytes(32);
@ -115,15 +171,15 @@ public class PaymentsManager :
//check if we have a db record with same pay hash but has the preimage set
var payHash = Convert.ToHexString(invoice.payment_hash()).ToLower();
var paySecret = Convert.ToHexString(invoice.payment_secret()).ToLower();
var payHash = new uint256(invoice.payment_hash());
var paySecret = new uint256(invoice.payment_secret());
await using var context = await _dbContextFactory.CreateDbContextAsync();
var inbound = await context.LightningPayments.FirstOrDefaultAsync(lightningPayment =>
lightningPayment.PaymentHash == payHash && lightningPayment.Inbound);
if (inbound is not null)
{
var newOutbound = new LightningPayment()
var newOutbound = new AppLightningPayment()
{
Inbound = false,
Value = amt,
@ -132,7 +188,7 @@ var paySecret = Convert.ToHexString(invoice.payment_secret()).ToLower();
Status = LightningPaymentStatus.Complete,
Timestamp = DateTimeOffset.UtcNow,
PaymentId = Convert.ToHexString(id).ToLower(),
PaymentRequests = [invoiceStr],
PaymentRequest = paymentRequest,
Preimage = inbound.Preimage
};
await context.LightningPayments.AddAsync(newOutbound);
@ -151,7 +207,7 @@ var paySecret = Convert.ToHexString(invoice.payment_secret()).ToLower();
return newOutbound;
}
var outbound = new LightningPayment()
var outbound = new AppLightningPayment()
{
Inbound = false,
Value = amt,
@ -160,7 +216,7 @@ var paySecret = Convert.ToHexString(invoice.payment_secret()).ToLower();
Status = LightningPaymentStatus.Pending,
Timestamp = DateTimeOffset.UtcNow,
PaymentId = Convert.ToHexString(id).ToLower(),
PaymentRequests = [invoiceStr],
PaymentRequest = paymentRequest,
};
await context.LightningPayments.AddAsync(outbound);
await context.SaveChangesAsync();
@ -197,41 +253,58 @@ var paySecret = Convert.ToHexString(invoice.payment_secret()).ToLower();
return outbound;
}
public async Task Cancel(string id, bool inbound)
public async Task Cancel(AppLightningPayment lightningPayment)
{
if (!inbound)
if (lightningPayment.Inbound)
{
await Task.Run(() => _channelManager.abandon_payment(Convert.FromHexString(id)) );
// return;
await CancelInbound(lightningPayment.PaymentHash);
}
else
{
await CancelOutbound(lightningPayment.PaymentId);
}
}
public async Task CancelInbound(uint256 paymentHash)
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
var payment = await context.LightningPayments.FirstOrDefaultAsync(lightningPayment =>
lightningPayment.Status == LightningPaymentStatus.Pending &&
((inbound && lightningPayment.Inbound && lightningPayment.PaymentHash == id) ||
(!inbound && !lightningPayment.Inbound && lightningPayment.PaymentId == id)));
lightningPayment.Status == LightningPaymentStatus.Pending && lightningPayment.Inbound && lightningPayment.PaymentHash == paymentHash);
if (payment is not null)
{
payment.Status = LightningPaymentStatus.Failed;
await context.SaveChangesAsync();
}
}
public async Task CancelOutbound(string paymentId)
{
await Task.Run(() => _channelManager.abandon_payment(Convert.FromHexString(paymentId)) );
await using var context = await _dbContextFactory.CreateDbContextAsync();
var payment = await context.LightningPayments.FirstOrDefaultAsync(lightningPayment =>
lightningPayment.Status == LightningPaymentStatus.Pending &&
!lightningPayment.Inbound && lightningPayment.PaymentId == paymentId);
if (payment is not null)
{
payment.Status = LightningPaymentStatus.Failed;
await context.SaveChangesAsync();
}
}
private async Task Payment(LightningPayment lightningPayment, CancellationToken cancellationToken = default)
private async Task Payment(AppLightningPayment lightningPayment, CancellationToken cancellationToken = default)
{
await using var context = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
var x = await context.LightningPayments.Upsert(lightningPayment).RunAsync(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);
}
}
private async Task PaymentUpdate(string paymentHash, bool inbound, string paymentId, bool failure,
private async Task PaymentUpdate(uint256 paymentHash, bool inbound, string paymentId, bool failure,
string? preimage, CancellationToken cancellationToken = default)
{
await using var context = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
@ -260,43 +333,93 @@ var paySecret = Convert.ToHexString(invoice.payment_secret()).ToLower();
}
}
public AsyncEventHandler<LightningPayment>? OnPaymentUpdate { get; set; }
public AsyncEventHandler<AppLightningPayment>? OnPaymentUpdate { get; set; }
public async Task Handle(Event.Event_PaymentClaimable eventPaymentClaimable)
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
var paymentHash = new uint256(eventPaymentClaimable.payment_hash);
var accept = await context.LightningPayments.FirstOrDefaultAsync(payment =>
payment.PaymentHash == Convert.ToHexString(eventPaymentClaimable.payment_hash).ToLower() &&
payment.PaymentHash == paymentHash &&
payment.Inbound && payment.Status == LightningPaymentStatus.Pending);
var preimage = eventPaymentClaimable.purpose.GetPreimage(out _) ??
(accept?.Preimage is not null ? Convert.FromHexString(accept.Preimage) : null);
if (accept is not null && preimage is not null)
_channelManager.claim_funds(preimage);
else
if (accept is null || preimage is null)
{
_channelManager.fail_htlc_backwards(eventPaymentClaimable.payment_hash);
return;
}
if (accept.Value == eventPaymentClaimable.amount_msat)
{
_channelManager.claim_funds(preimage);
return;
}
//this discrepancy could have been used to pay for a JIT channel opening
else if(_acceptedChannels.TryGetValue(eventPaymentClaimable.via_channel_id.hash(), out var channelRequest) &&
accept.AdditionalData.TryGetValue(VoltageFlow2Jit.LightningPaymentLSPKey, out var lspDoc ) &&
lspDoc.Deserialize<string>() is { } lsp &&
await _ldkNode.GetJITLSPService() is { } lspService &&
lspService.ProviderName == lsp &&
accept.AdditionalData.TryGetValue(VoltageFlow2Jit.LightningPaymentJITFeeKey, out var lspFee ) && lspFee.Deserialize<JITFeeResponse>() is { } fee)
{
if (fee.AmountToGenerateOurInvoice == eventPaymentClaimable.amount_msat)
{
_acceptedChannels.Remove(eventPaymentClaimable.via_channel_id.hash(), out _);
_channelManager.claim_funds(preimage);
return;
}
}
else
{
_channelManager.fail_htlc_backwards(eventPaymentClaimable.payment_hash);
}
_channelManager.fail_htlc_backwards(eventPaymentClaimable.payment_hash);
}
public async Task Handle(Event.Event_PaymentClaimed eventPaymentClaimed)
{
var preimage = eventPaymentClaimed.purpose.GetPreimage(out var secret);
await PaymentUpdate( Convert.ToHexString(eventPaymentClaimed.payment_hash).ToLower(), true, "default", false, preimage is null ? null : Convert.ToHexString(preimage).ToLower());
await PaymentUpdate( new uint256(eventPaymentClaimed.payment_hash), true, "default", false, preimage is null ? null : Convert.ToHexString(preimage).ToLower());
}
public async Task Handle(Event.Event_PaymentFailed @eventPaymentFailed)
{
await PaymentUpdate(Convert.ToHexString(eventPaymentFailed.payment_hash).ToLower(), false,
await PaymentUpdate(new uint256(eventPaymentFailed.payment_hash), false,
Convert.ToHexString(eventPaymentFailed.payment_id).ToLower(), true, null);
}
public async Task Handle(Event.Event_PaymentSent eventPaymentSent)
{
await PaymentUpdate(Convert.ToHexString(eventPaymentSent.payment_hash).ToLower(), false,
await PaymentUpdate(new uint256(eventPaymentSent.payment_hash), false,
Convert.ToHexString(
((Option_ThirtyTwoBytesZ.Option_ThirtyTwoBytesZ_Some) eventPaymentSent.payment_id).some).ToLower(), false,
Convert.ToHexString(eventPaymentSent.payment_preimage).ToLower());
}
public async Task StartAsync(CancellationToken cancellationToken)
{
_openChannelRequestEventHandler.AcceptedChannel += AcceptedChannel;
}
private ConcurrentDictionary<long, Event.Event_OpenChannelRequest> _acceptedChannels = new();
private Task AcceptedChannel(object? sender, Event.Event_OpenChannelRequest e)
{
_acceptedChannels.TryAdd(e.temporary_channel_id.hash(), e);
return Task.CompletedTask;
}
public async Task StopAsync(CancellationToken cancellationToken)
{
_openChannelRequestEventHandler.AcceptedChannel -= AcceptedChannel;
}
}

View File

@ -1,25 +0,0 @@
using BTCPayApp.Core.Attempt2;
using BTCPayServer.Lightning;
namespace BTCPayApp.Core.LSP.JIT;
public class BTCPayJIT: IJITService
{
public BTCPayJIT(BTCPayConnectionManager btcPayConnectionManager)
{
}
public string ProviderName => "BTCPayServer";
public async Task<BOLT11PaymentRequest> WrapInvoice(BOLT11PaymentRequest invoice)
{
throw new NotImplementedException();
}
}
public interface IJITService
{
public string ProviderName { get; }
public Task<BOLT11PaymentRequest> WrapInvoice(BOLT11PaymentRequest invoice);
}

View File

@ -0,0 +1,203 @@
using System.Net;
using System.Text.Json;
using BTCPayApp.Core.Attempt2;
using BTCPayApp.Core.Data;
using BTCPayApp.Core.Helpers;
using BTCPayApp.Core.LDK;
using BTCPayServer.Lightning;
using Microsoft.Extensions.Logging;
using NBitcoin;
using Newtonsoft.Json;
using org.ldk.structs;
using JsonSerializer = System.Text.Json.JsonSerializer;
namespace BTCPayApp.Core.LSP.JIT;
/// <summary>
/// https://docs.voltage.cloud/flow/flow-2.0
/// </summary>
public class VoltageFlow2Jit : IJITService, IScopedHostedService, ILDKEventHandler<Event.Event_ChannelPending>
{
private readonly HttpClient _httpClient;
private readonly Network _network;
private readonly LDKNode _node;
private readonly ChannelManager _channelManager;
private readonly ILogger<VoltageFlow2Jit> _logger;
public static Uri? BaseAddress(Network network)
{
return network switch
{
not null when network == Network.Main => new Uri("https://lsp.voltageapi.com"),
not null when network == Network.TestNet => new Uri("https://testnet-lsp.voltageapi.com"),
// not null when network == Network.RegTest => new Uri("https://localhost:5001/jit-lsp"),
_ => null
};
}
public VoltageFlow2Jit(IHttpClientFactory httpClientFactory, Network network, LDKNode node,
ChannelManager channelManager, ILogger<VoltageFlow2Jit> logger)
{
var httpClientInstance = httpClientFactory.CreateClient("VoltageFlow2JIT");
httpClientInstance.BaseAddress = BaseAddress(network);
_httpClient = httpClientInstance;
_network = network;
_node = node;
_channelManager = channelManager;
_logger = logger;
}
public async Task<FlowInfoResponse> GetInfo(CancellationToken cancellationToken = default)
{
var path = "/api/v1/info";
var response = await _httpClient.GetAsync(path, cancellationToken);
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync(cancellationToken);
return JsonConvert.DeserializeObject<FlowInfoResponse>(content);
}
public async Task<FlowFeeResponse> GetFee(LightMoney amount, PubKey pubkey,
CancellationToken cancellationToken = default)
{
var path = "/api/v1/fee";
var request = new FlowFeeRequest(amount, pubkey);
var response = await _httpClient.PostAsync(path, new StringContent(JsonConvert.SerializeObject(request)),
cancellationToken);
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync(cancellationToken);
return JsonConvert.DeserializeObject<FlowFeeResponse>(content);
}
public async Task<BOLT11PaymentRequest> GetProposal(BOLT11PaymentRequest bolt11PaymentRequest,
EndPoint? endPoint = null, string? feeId = null, CancellationToken cancellationToken = default)
{
var path = "/api/v1/proposal";
var request = new FlowProposalRequest()
{
Bolt11 = bolt11PaymentRequest.ToString(),
Host = endPoint?.Host(),
Port = endPoint?.Port(),
FeeId = feeId,
};
var response = await _httpClient
.PostAsync(path, new StringContent(JsonConvert.SerializeObject(request)), cancellationToken);
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync(cancellationToken);
var result = JsonConvert.DeserializeObject<FlowProposalResponse>(content);
return BOLT11PaymentRequest.Parse(result!.WrappedBolt11, _network);
}
public string ProviderName => "Voltage";
public async Task<JITFeeResponse?> CalculateInvoiceAmount(LightMoney expectedAmount)
{
try
{
var fee = await GetFee(expectedAmount, _node.NodeId);
return new JITFeeResponse(expectedAmount, expectedAmount + fee.Amount, fee.Amount, fee.Id, ProviderName);
}
catch (Exception e)
{
_logger.LogError(e, "Error while calculating invoice amount");
return null;
}
}
public const string LightningPaymentJITFeeKey = "JITFeeKey";
public const string LightningPaymentLSPKey = "LSP";
public const string LightningPaymentOriginalPaymentRequest = "OriginalPaymentRequest";
public async Task<bool> WrapInvoice(AppLightningPayment lightningPayment, JITFeeResponse? fee)
{
if(lightningPayment.AdditionalData?.ContainsKey(LightningPaymentLSPKey) is true)
return false;
fee??= await CalculateInvoiceAmount(new LightMoney(lightningPayment.Value));
if(fee is null)
return false;
var invoice = lightningPayment.PaymentRequest;
var proposal = await GetProposal(invoice,null, fee!.FeeIdentifier);
if(proposal.MinimumAmount != fee.AmountToRequestPayer || proposal.PaymentHash != invoice.PaymentHash)
return false;
lightningPayment.PaymentRequest = proposal;
lightningPayment.AdditionalData ??= new Dictionary<string, JsonElement>();
lightningPayment.AdditionalData[LightningPaymentOriginalPaymentRequest] = JsonSerializer.SerializeToElement(invoice);
lightningPayment.AdditionalData[LightningPaymentLSPKey] = JsonSerializer.SerializeToElement(ProviderName);
lightningPayment.AdditionalData[LightningPaymentJITFeeKey] = JsonSerializer.SerializeToElement(fee);
return true;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
_node.ConfigUpdated += ConfigUpdated;
_ = ConfigUpdated(this, await _node.GetConfig()).WithCancellation(cancellationToken);
}
private FlowInfoResponse? _info;
private async Task ConfigUpdated(object? sender, LightningConfig e)
{
if (e.JITLSP == ProviderName)
{
_info ??= await GetInfo();
var ni = _info.ToNodeInfo();
var configPeers = await _node.GetConfig();
var pubkey = new PubKey(_info.PubKey);
if (configPeers.Peers.TryGetValue(_info.PubKey, out var peer))
{
//check if the endpoint matches any of the info ones
if(!_info.ConnectionMethods.Any(a => a.ToEndpoint().ToEndpointString().Equals(peer.Endpoint, StringComparison.OrdinalIgnoreCase)))
{
peer = new PeerInfo {Endpoint = _info.ConnectionMethods.First().ToEndpoint().ToEndpointString(), Persistent = true, Trusted = true};
}else if (peer is {Persistent: true, Trusted: true})
return;
else
{
peer = peer with
{
Persistent = true,
Trusted = true
};
}
}
else
{
peer = new PeerInfo {Endpoint = _info.ConnectionMethods.First().ToEndpoint().ToEndpointString(), Persistent = true, Trusted = true};
}
_ = _node.Peer(pubkey, peer);
}
}
public async Task StopAsync(CancellationToken cancellationToken)
{
_node.ConfigUpdated -= ConfigUpdated;
}
public async Task Handle(Event.Event_ChannelPending @event)
{
var nodeId = new PubKey(@event.counterparty_node_id);
if(nodeId.ToString() == _info?.PubKey)
{
var channel = _channelManager
.list_channels_with_counterparty(@event.counterparty_node_id)
.FirstOrDefault(a => a.get_channel_id().eq(@event.channel_id));
if(channel is null)
return;
var channelConfig = channel.get_config();
channelConfig.set_accept_underpaying_htlcs(true);
_channelManager.update_channel_config(@event.counterparty_node_id, new[] {@event.channel_id}, channelConfig);
}
}
}

View File

@ -0,0 +1,21 @@
using BTCPayServer.Lightning;
using NBitcoin;
using Newtonsoft.Json;
namespace BTCPayApp.Core.LSP.JIT;
public class FlowFeeRequest
{
public FlowFeeRequest()
{
}
public FlowFeeRequest(LightMoney amount, PubKey pubkey)
{
Amount = amount.MilliSatoshi;
PubKey = pubkey.ToHex();
}
[JsonProperty("amount_msat")] public long Amount { get; set; }
[JsonProperty("pubkey")] public string PubKey { get; set; }
}

View File

@ -0,0 +1,9 @@
using Newtonsoft.Json;
namespace BTCPayApp.Core.LSP.JIT;
public class FlowFeeResponse
{
[JsonProperty("amount_msat")] public long Amount { get; set; }
[JsonProperty("id")] public required string Id { get; set; }
}

View File

@ -0,0 +1,31 @@
using System.Net;
using BTCPayApp.Core.Helpers;
using BTCPayServer.Lightning;
using NBitcoin;
using Newtonsoft.Json;
namespace BTCPayApp.Core.LSP.JIT;
public class FlowInfoResponse
{
[JsonProperty("connection_methods")] public ConnectionMethod[] ConnectionMethods { get; set; }
[JsonProperty("pubkey")] public required string PubKey { get; set; }
public NodeInfo[] ToNodeInfo()
{
var pubkey = new PubKey(PubKey);
return ConnectionMethods.Select(method => new NodeInfo(pubkey, method.Address, method.Port)).ToArray();
}
public class ConnectionMethod
{
[JsonProperty("address")] public string Address { get; set; }
[JsonProperty("port")] public int Port { get; set; }
[JsonProperty("type")] public string Type { get; set; }
public EndPoint? ToEndpoint()
{
return EndPointParser.TryParse($"{Address}:{Port}", 9735, out var endpoint) ? endpoint : null;
}
}
}

View File

@ -0,0 +1,18 @@
using Newtonsoft.Json;
namespace BTCPayApp.Core.LSP.JIT;
public class FlowProposalRequest
{
[JsonProperty("bolt11")] public required string Bolt11 { get; set; }
[JsonProperty("host", NullValueHandling = NullValueHandling.Ignore)]
public string? Host { get; set; }
[JsonProperty("port", NullValueHandling = NullValueHandling.Ignore)]
public int? Port { get; set; }
[JsonProperty("fee_id", NullValueHandling = NullValueHandling.Ignore)]
public string? FeeId { get; set; }
}

View File

@ -0,0 +1,8 @@
using Newtonsoft.Json;
namespace BTCPayApp.Core.LSP.JIT;
public class FlowProposalResponse
{
[JsonProperty("jit_bolt11")] public required string WrappedBolt11 { get; set; }
}

View File

@ -0,0 +1,48 @@
using System.Text.Json.Serialization;
using BTCPayApp.Core.Data;
using BTCPayApp.Core.JsonConverters;
using BTCPayServer.Lightning;
namespace BTCPayApp.Core.LSP.JIT;
public interface IJITService
{
public string ProviderName { get; }
public Task<JITFeeResponse?> CalculateInvoiceAmount(LightMoney expectedAmount);
public Task<bool> WrapInvoice(AppLightningPayment lightningPayment, JITFeeResponse? feeReponse);
}
public record JITFeeResponse
{
public JITFeeResponse(LightMoney AmountToRequestPayer, LightMoney AmountToGenerateOurInvoice, LightMoney LSPFee,
string FeeIdentifier, string LSP)
{
this.AmountToRequestPayer = AmountToRequestPayer;
this.AmountToGenerateOurInvoice = AmountToGenerateOurInvoice;
this.LSPFee = LSPFee;
this.FeeIdentifier = FeeIdentifier;
this.LSP = LSP;
}
[JsonConverter(typeof(LightMoneyJsonConverter))]
public LightMoney AmountToRequestPayer { get; init; }
[JsonConverter(typeof(LightMoneyJsonConverter))]
public LightMoney AmountToGenerateOurInvoice { get; init; }
[JsonConverter(typeof(LightMoneyJsonConverter))]
public LightMoney LSPFee { get; init; }
public string FeeIdentifier { get; init; }
public string LSP { get; set; }
public void Deconstruct(out LightMoney AmountToRequestPayer, out LightMoney AmountToGenerateOurInvoice,
out LightMoney LSPFee, out string FeeIdentifier)
{
AmountToRequestPayer = this.AmountToRequestPayer;
AmountToGenerateOurInvoice = this.AmountToGenerateOurInvoice;
LSPFee = this.LSPFee;
FeeIdentifier = this.FeeIdentifier;
}
}

View File

@ -1,88 +0,0 @@
// <auto-generated />
using System;
using BTCPayApp.Core.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace BTCPayApp.Core.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20240502121610_Init")]
partial class Init
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "8.0.4");
modelBuilder.Entity("BTCPayApp.Core.Data.Channel", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<string>("Aliases")
.IsRequired()
.HasColumnType("TEXT");
b.Property<byte[]>("Data")
.IsRequired()
.HasColumnType("BLOB");
b.HasKey("Id");
b.ToTable("LightningChannels");
});
modelBuilder.Entity("BTCPayApp.Core.Data.LightningPayment", b =>
{
b.Property<string>("PaymentHash")
.HasColumnType("TEXT");
b.Property<bool>("Inbound")
.HasColumnType("INTEGER");
b.Property<string>("PaymentId")
.HasColumnType("TEXT");
b.Property<string>("Preimage")
.HasColumnType("TEXT");
b.Property<string>("Secret")
.HasColumnType("TEXT");
b.Property<int>("Status")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("Timestamp")
.HasColumnType("TEXT");
b.Property<long>("Value")
.HasColumnType("INTEGER");
b.HasKey("PaymentHash", "Inbound");
b.ToTable("LightningPayments");
});
modelBuilder.Entity("BTCPayApp.Core.Data.Setting", b =>
{
b.Property<string>("Key")
.HasColumnType("TEXT");
b.Property<byte[]>("Value")
.IsRequired()
.HasColumnType("BLOB");
b.HasKey("Key");
b.ToTable("Settings");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -1,92 +0,0 @@
// <auto-generated />
using System;
using BTCPayApp.Core.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace BTCPayApp.Core.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20240508085527_PR")]
partial class PR
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "8.0.4");
modelBuilder.Entity("BTCPayApp.CommonServer.LightningPayment", b =>
{
b.Property<string>("PaymentHash")
.HasColumnType("TEXT");
b.Property<bool>("Inbound")
.HasColumnType("INTEGER");
b.Property<string>("PaymentId")
.HasColumnType("TEXT");
b.Property<string>("PaymentRequests")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Preimage")
.HasColumnType("TEXT");
b.Property<string>("Secret")
.HasColumnType("TEXT");
b.Property<int>("Status")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("Timestamp")
.HasColumnType("TEXT");
b.Property<long>("Value")
.HasColumnType("INTEGER");
b.HasKey("PaymentHash", "Inbound");
b.ToTable("LightningPayments");
});
modelBuilder.Entity("BTCPayApp.Core.Data.Channel", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<string>("Aliases")
.IsRequired()
.HasColumnType("TEXT");
b.Property<byte[]>("Data")
.IsRequired()
.HasColumnType("BLOB");
b.HasKey("Id");
b.ToTable("LightningChannels");
});
modelBuilder.Entity("BTCPayApp.Core.Data.Setting", b =>
{
b.Property<string>("Key")
.HasColumnType("TEXT");
b.Property<byte[]>("Value")
.IsRequired()
.HasColumnType("BLOB");
b.HasKey("Key");
b.ToTable("Settings");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -1,29 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace BTCPayApp.Core.Migrations
{
/// <inheritdoc />
public partial class PR : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "PaymentRequests",
table: "LightningPayments",
type: "TEXT",
nullable: false,
defaultValue: "[]");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "PaymentRequests",
table: "LightningPayments");
}
}
}

View File

@ -1,92 +0,0 @@
// <auto-generated />
using System;
using BTCPayApp.Core.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace BTCPayApp.Core.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20240527105733_upd2")]
partial class upd2
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "8.0.5");
modelBuilder.Entity("BTCPayApp.CommonServer.LightningPayment", b =>
{
b.Property<string>("PaymentHash")
.HasColumnType("TEXT");
b.Property<bool>("Inbound")
.HasColumnType("INTEGER");
b.Property<string>("PaymentId")
.HasColumnType("TEXT");
b.Property<string>("PaymentRequests")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Preimage")
.HasColumnType("TEXT");
b.Property<string>("Secret")
.HasColumnType("TEXT");
b.Property<int>("Status")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("Timestamp")
.HasColumnType("TEXT");
b.Property<long>("Value")
.HasColumnType("INTEGER");
b.HasKey("PaymentHash", "Inbound", "PaymentId");
b.ToTable("LightningPayments");
});
modelBuilder.Entity("BTCPayApp.Core.Data.Channel", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<string>("Aliases")
.IsRequired()
.HasColumnType("TEXT");
b.Property<byte[]>("Data")
.IsRequired()
.HasColumnType("BLOB");
b.HasKey("Id");
b.ToTable("LightningChannels");
});
modelBuilder.Entity("BTCPayApp.Core.Data.Setting", b =>
{
b.Property<string>("Key")
.HasColumnType("TEXT");
b.Property<byte[]>("Value")
.IsRequired()
.HasColumnType("BLOB");
b.HasKey("Key");
b.ToTable("Settings");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -1,54 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace BTCPayApp.Core.Migrations
{
/// <inheritdoc />
public partial class upd2 : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropPrimaryKey(
name: "PK_LightningPayments",
table: "LightningPayments");
migrationBuilder.AlterColumn<string>(
name: "PaymentId",
table: "LightningPayments",
type: "TEXT",
nullable: false,
defaultValue: "",
oldClrType: typeof(string),
oldType: "TEXT",
oldNullable: true);
migrationBuilder.AddPrimaryKey(
name: "PK_LightningPayments",
table: "LightningPayments",
columns: new[] { "PaymentHash", "Inbound", "PaymentId" });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropPrimaryKey(
name: "PK_LightningPayments",
table: "LightningPayments");
migrationBuilder.AlterColumn<string>(
name: "PaymentId",
table: "LightningPayments",
type: "TEXT",
nullable: true,
oldClrType: typeof(string),
oldType: "TEXT");
migrationBuilder.AddPrimaryKey(
name: "PK_LightningPayments",
table: "LightningPayments",
columns: new[] { "PaymentHash", "Inbound" });
}
}
}

View File

@ -1,29 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace BTCPayApp.Core.Migrations
{
/// <inheritdoc />
public partial class adddatatoln : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "AdditionalData",
table: "LightningPayments",
type: "TEXT",
nullable: false,
defaultValue: "");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "AdditionalData",
table: "LightningPayments");
}
}
}

View File

@ -11,8 +11,8 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace BTCPayApp.Core.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20240621120336_adddatatoln")]
partial class adddatatoln
[Migration("20240627110659_Init")]
partial class Init
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
@ -20,45 +20,6 @@ namespace BTCPayApp.Core.Migrations
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "8.0.6");
modelBuilder.Entity("BTCPayApp.CommonServer.Models.LightningPayment", b =>
{
b.Property<string>("PaymentHash")
.HasColumnType("TEXT");
b.Property<bool>("Inbound")
.HasColumnType("INTEGER");
b.Property<string>("PaymentId")
.HasColumnType("TEXT");
b.Property<string>("AdditionalData")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("PaymentRequests")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Preimage")
.HasColumnType("TEXT");
b.Property<string>("Secret")
.HasColumnType("TEXT");
b.Property<int>("Status")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("Timestamp")
.HasColumnType("TEXT");
b.Property<long>("Value")
.HasColumnType("INTEGER");
b.HasKey("PaymentHash", "Inbound", "PaymentId");
b.ToTable("LightningPayments");
});
modelBuilder.Entity("BTCPayApp.Core.Data.Channel", b =>
{
b.Property<string>("Id")
@ -90,6 +51,46 @@ namespace BTCPayApp.Core.Migrations
b.ToTable("Settings");
});
modelBuilder.Entity("BTCPayApp.Core.LDK.AppLightningPayment", b =>
{
b.Property<string>("PaymentHash")
.HasColumnType("TEXT");
b.Property<bool>("Inbound")
.HasColumnType("INTEGER");
b.Property<string>("PaymentId")
.HasColumnType("TEXT");
b.Property<string>("AdditionalData")
.IsRequired()
.HasColumnType("jsonb");
b.Property<string>("PaymentRequest")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Preimage")
.HasColumnType("TEXT");
b.Property<string>("Secret")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("Status")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("Timestamp")
.HasColumnType("TEXT");
b.Property<long>("Value")
.HasColumnType("INTEGER");
b.HasKey("PaymentHash", "Inbound", "PaymentId");
b.ToTable("LightningPayments");
});
#pragma warning restore 612, 618
}
}

View File

@ -29,17 +29,19 @@ namespace BTCPayApp.Core.Migrations
columns: table => new
{
PaymentHash = table.Column<string>(type: "TEXT", nullable: false),
PaymentId = table.Column<string>(type: "TEXT", nullable: false),
Inbound = table.Column<bool>(type: "INTEGER", nullable: false),
PaymentId = table.Column<string>(type: "TEXT", nullable: true),
Preimage = table.Column<string>(type: "TEXT", nullable: true),
Secret = table.Column<string>(type: "TEXT", nullable: true),
Secret = table.Column<string>(type: "TEXT", nullable: false),
Timestamp = table.Column<DateTimeOffset>(type: "TEXT", nullable: false),
Value = table.Column<long>(type: "INTEGER", nullable: false),
Status = table.Column<int>(type: "INTEGER", nullable: false)
Status = table.Column<int>(type: "INTEGER", nullable: false),
PaymentRequest = table.Column<string>(type: "TEXT", nullable: false),
AdditionalData = table.Column<string>(type: "jsonb", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_LightningPayments", x => new { x.PaymentHash, x.Inbound });
table.PrimaryKey("PK_LightningPayments", x => new { x.PaymentHash, x.Inbound, x.PaymentId });
});
migrationBuilder.CreateTable(

View File

@ -0,0 +1,213 @@
// <auto-generated />
using System;
using BTCPayApp.Core.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace BTCPayApp.Core.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20240726135741_triggers")]
partial class triggers
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "8.0.6");
modelBuilder.Entity("BTCPayApp.Core.Data.AppLightningPayment", b =>
{
b.Property<string>("PaymentHash")
.HasColumnType("TEXT");
b.Property<bool>("Inbound")
.HasColumnType("INTEGER");
b.Property<string>("PaymentId")
.HasColumnType("TEXT");
b.Property<string>("AdditionalData")
.IsRequired()
.HasColumnType("jsonb");
b.Property<string>("EntityKey")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("PaymentRequest")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Preimage")
.HasColumnType("TEXT");
b.Property<string>("Secret")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("Status")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("Timestamp")
.HasColumnType("TEXT");
b.Property<long>("Value")
.HasColumnType("INTEGER");
b.Property<long>("Version")
.HasColumnType("INTEGER");
b.HasKey("PaymentHash", "Inbound", "PaymentId");
b.ToTable("LightningPayments", t =>
{
t.HasTrigger("LC_TRIGGER_AFTER_DELETE_APPLIGHTNINGPAYMENT");
t.HasTrigger("LC_TRIGGER_AFTER_INSERT_APPLIGHTNINGPAYMENT");
t.HasTrigger("LC_TRIGGER_AFTER_UPDATE_APPLIGHTNINGPAYMENT");
});
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.\"EntityKey\", \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.\"EntityKey\", \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\n INSERT INTO \"OutboxItems\" (\"Entity\", \"Version\", \"Key\", \"ActionType\") SELECT 'Payment', \r\n OLD.\"Version\" + 1, \r\n NEW.\"EntityKey\", \r\n 1;\r\nEND;");
});
modelBuilder.Entity("BTCPayApp.Core.Data.Channel", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<byte[]>("Data")
.IsRequired()
.HasColumnType("BLOB");
b.Property<string>("EntityKey")
.IsRequired()
.HasColumnType("TEXT");
b.Property<long>("Version")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.ToTable("LightningChannels", t =>
{
t.HasTrigger("LC_TRIGGER_AFTER_DELETE_CHANNEL");
t.HasTrigger("LC_TRIGGER_AFTER_INSERT_CHANNEL");
t.HasTrigger("LC_TRIGGER_AFTER_UPDATE_CHANNEL");
});
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.\"EntityKey\", \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.\"EntityKey\", \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\n INSERT INTO \"OutboxItems\" (\"Entity\", \"Version\", \"Key\", \"ActionType\") SELECT 'Channel', \r\n OLD.\"Version\" + 1, \r\n NEW.\"EntityKey\", \r\n 1;\r\nEND;");
});
modelBuilder.Entity("BTCPayApp.Core.Data.ChannelAlias", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<string>("ChannelId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Type")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("ChannelId");
b.ToTable("ChannelAliases");
});
modelBuilder.Entity("BTCPayApp.Core.Data.Outbox", b =>
{
b.Property<string>("Entity")
.HasColumnType("TEXT");
b.Property<string>("Key")
.HasColumnType("TEXT");
b.Property<int>("ActionType")
.HasColumnType("INTEGER");
b.Property<long>("Version")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("Timestamp")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValueSql("datetime('now')");
b.HasKey("Entity", "Key", "ActionType", "Version");
b.ToTable("OutboxItems");
});
modelBuilder.Entity("BTCPayApp.Core.Data.Setting", b =>
{
b.Property<string>("Key")
.HasColumnType("TEXT");
b.Property<bool>("Backup")
.HasColumnType("INTEGER");
b.Property<string>("EntityKey")
.IsRequired()
.HasColumnType("TEXT");
b.Property<byte[]>("Value")
.IsRequired()
.HasColumnType("BLOB");
b.Property<long>("Version")
.HasColumnType("INTEGER");
b.HasKey("Key");
b.ToTable("Settings", t =>
{
t.HasTrigger("LC_TRIGGER_AFTER_DELETE_SETTING");
t.HasTrigger("LC_TRIGGER_AFTER_INSERT_SETTING");
t.HasTrigger("LC_TRIGGER_AFTER_UPDATE_SETTING");
});
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.\"EntityKey\", \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.\"EntityKey\", \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\nWHEN \r\n \r\n OLD.\"Backup\" IS TRUE\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.\"EntityKey\", \r\n 1;\r\nEND;");
});
modelBuilder.Entity("BTCPayApp.Core.Data.ChannelAlias", b =>
{
b.HasOne("BTCPayApp.Core.Data.Channel", "Channel")
.WithMany("Aliases")
.HasForeignKey("ChannelId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Channel");
});
modelBuilder.Entity("BTCPayApp.Core.Data.Channel", b =>
{
b.Navigation("Aliases");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,176 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace BTCPayApp.Core.Migrations
{
/// <inheritdoc />
public partial class triggers : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.RenameColumn(
name: "Aliases",
table: "LightningChannels",
newName: "EntityKey");
migrationBuilder.AddColumn<bool>(
name: "Backup",
table: "Settings",
type: "INTEGER",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<string>(
name: "EntityKey",
table: "Settings",
type: "TEXT",
nullable: false,
defaultValue: "");
migrationBuilder.AddColumn<long>(
name: "Version",
table: "Settings",
type: "INTEGER",
nullable: false,
defaultValue: 0L);
migrationBuilder.AddColumn<string>(
name: "EntityKey",
table: "LightningPayments",
type: "TEXT",
nullable: false,
defaultValue: "");
migrationBuilder.AddColumn<long>(
name: "Version",
table: "LightningPayments",
type: "INTEGER",
nullable: false,
defaultValue: 0L);
migrationBuilder.AddColumn<long>(
name: "Version",
table: "LightningChannels",
type: "INTEGER",
nullable: false,
defaultValue: 0L);
migrationBuilder.CreateTable(
name: "ChannelAliases",
columns: table => new
{
Id = table.Column<string>(type: "TEXT", nullable: false),
Type = table.Column<string>(type: "TEXT", nullable: false),
ChannelId = table.Column<string>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ChannelAliases", x => x.Id);
table.ForeignKey(
name: "FK_ChannelAliases_LightningChannels_ChannelId",
column: x => x.ChannelId,
principalTable: "LightningChannels",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "OutboxItems",
columns: table => new
{
ActionType = table.Column<int>(type: "INTEGER", nullable: false),
Key = table.Column<string>(type: "TEXT", nullable: false),
Entity = table.Column<string>(type: "TEXT", nullable: false),
Version = table.Column<long>(type: "INTEGER", nullable: false),
Timestamp = table.Column<DateTimeOffset>(type: "TEXT", nullable: false, defaultValueSql: "datetime('now')")
},
constraints: table =>
{
table.PrimaryKey("PK_OutboxItems", x => new { x.Entity, x.Key, x.ActionType, x.Version });
});
migrationBuilder.CreateIndex(
name: "IX_ChannelAliases_ChannelId",
table: "ChannelAliases",
column: "ChannelId");
migrationBuilder.Sql("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.\"EntityKey\", \r\n 2;\r\nEND;");
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.\"EntityKey\", \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\n INSERT INTO \"OutboxItems\" (\"Entity\", \"Version\", \"Key\", \"ActionType\") SELECT 'Payment', \r\n OLD.\"Version\" + 1, \r\n NEW.\"EntityKey\", \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.\"EntityKey\", \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.\"EntityKey\", \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\n INSERT INTO \"OutboxItems\" (\"Entity\", \"Version\", \"Key\", \"ActionType\") SELECT 'Channel', \r\n OLD.\"Version\" + 1, \r\n NEW.\"EntityKey\", \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.\"EntityKey\", \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.\"EntityKey\", \r\n 0;\r\nEND;");
migrationBuilder.Sql("CREATE TRIGGER LC_TRIGGER_AFTER_UPDATE_SETTING\r\nAFTER UPDATE ON \"Settings\"\r\nFOR EACH ROW\r\nWHEN \r\n \r\n OLD.\"Backup\" IS TRUE\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.\"EntityKey\", \r\n 1;\r\nEND;");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("PRAGMA writable_schema = 1; \r\nDELETE FROM sqlite_master WHERE type = 'trigger' AND name like 'LC_TRIGGER_AFTER_DELETE_APPLIGHTNINGPAYMENT%';\r\nPRAGMA writable_schema = 0;");
migrationBuilder.Sql("PRAGMA writable_schema = 1; \r\nDELETE FROM sqlite_master WHERE type = 'trigger' AND name like 'LC_TRIGGER_AFTER_INSERT_APPLIGHTNINGPAYMENT%';\r\nPRAGMA writable_schema = 0;");
migrationBuilder.Sql("PRAGMA writable_schema = 1; \r\nDELETE FROM sqlite_master WHERE type = 'trigger' AND name like 'LC_TRIGGER_AFTER_UPDATE_APPLIGHTNINGPAYMENT%';\r\nPRAGMA writable_schema = 0;");
migrationBuilder.Sql("PRAGMA writable_schema = 1; \r\nDELETE FROM sqlite_master WHERE type = 'trigger' AND name like 'LC_TRIGGER_AFTER_DELETE_CHANNEL%';\r\nPRAGMA writable_schema = 0;");
migrationBuilder.Sql("PRAGMA writable_schema = 1; \r\nDELETE FROM sqlite_master WHERE type = 'trigger' AND name like 'LC_TRIGGER_AFTER_INSERT_CHANNEL%';\r\nPRAGMA writable_schema = 0;");
migrationBuilder.Sql("PRAGMA writable_schema = 1; \r\nDELETE FROM sqlite_master WHERE type = 'trigger' AND name like 'LC_TRIGGER_AFTER_UPDATE_CHANNEL%';\r\nPRAGMA writable_schema = 0;");
migrationBuilder.Sql("PRAGMA writable_schema = 1; \r\nDELETE FROM sqlite_master WHERE type = 'trigger' AND name like 'LC_TRIGGER_AFTER_DELETE_SETTING%';\r\nPRAGMA writable_schema = 0;");
migrationBuilder.Sql("PRAGMA writable_schema = 1; \r\nDELETE FROM sqlite_master WHERE type = 'trigger' AND name like 'LC_TRIGGER_AFTER_INSERT_SETTING%';\r\nPRAGMA writable_schema = 0;");
migrationBuilder.Sql("PRAGMA writable_schema = 1; \r\nDELETE FROM sqlite_master WHERE type = 'trigger' AND name like 'LC_TRIGGER_AFTER_UPDATE_SETTING%';\r\nPRAGMA writable_schema = 0;");
migrationBuilder.DropTable(
name: "ChannelAliases");
migrationBuilder.DropTable(
name: "OutboxItems");
migrationBuilder.DropColumn(
name: "Backup",
table: "Settings");
migrationBuilder.DropColumn(
name: "EntityKey",
table: "Settings");
migrationBuilder.DropColumn(
name: "Version",
table: "Settings");
migrationBuilder.DropColumn(
name: "EntityKey",
table: "LightningPayments");
migrationBuilder.DropColumn(
name: "Version",
table: "LightningPayments");
migrationBuilder.DropColumn(
name: "Version",
table: "LightningChannels");
migrationBuilder.RenameColumn(
name: "EntityKey",
table: "LightningChannels",
newName: "Aliases");
}
}
}

View File

@ -17,7 +17,7 @@ namespace BTCPayApp.Core.Migrations
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "8.0.6");
modelBuilder.Entity("BTCPayApp.CommonServer.Models.LightningPayment", b =>
modelBuilder.Entity("BTCPayApp.Core.Data.AppLightningPayment", b =>
{
b.Property<string>("PaymentHash")
.HasColumnType("TEXT");
@ -29,10 +29,14 @@ namespace BTCPayApp.Core.Migrations
.HasColumnType("TEXT");
b.Property<string>("AdditionalData")
.IsRequired()
.HasColumnType("jsonb");
b.Property<string>("EntityKey")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("PaymentRequests")
b.Property<string>("PaymentRequest")
.IsRequired()
.HasColumnType("TEXT");
@ -40,6 +44,7 @@ namespace BTCPayApp.Core.Migrations
.HasColumnType("TEXT");
b.Property<string>("Secret")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("Status")
@ -51,9 +56,24 @@ namespace BTCPayApp.Core.Migrations
b.Property<long>("Value")
.HasColumnType("INTEGER");
b.Property<long>("Version")
.HasColumnType("INTEGER");
b.HasKey("PaymentHash", "Inbound", "PaymentId");
b.ToTable("LightningPayments");
b.ToTable("LightningPayments", t =>
{
t.HasTrigger("LC_TRIGGER_AFTER_DELETE_APPLIGHTNINGPAYMENT");
t.HasTrigger("LC_TRIGGER_AFTER_INSERT_APPLIGHTNINGPAYMENT");
t.HasTrigger("LC_TRIGGER_AFTER_UPDATE_APPLIGHTNINGPAYMENT");
});
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.\"EntityKey\", \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.\"EntityKey\", \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\n INSERT INTO \"OutboxItems\" (\"Entity\", \"Version\", \"Key\", \"ActionType\") SELECT 'Payment', \r\n OLD.\"Version\" + 1, \r\n NEW.\"EntityKey\", \r\n 1;\r\nEND;");
});
modelBuilder.Entity("BTCPayApp.Core.Data.Channel", b =>
@ -61,17 +81,76 @@ namespace BTCPayApp.Core.Migrations
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<string>("Aliases")
.IsRequired()
.HasColumnType("TEXT");
b.Property<byte[]>("Data")
.IsRequired()
.HasColumnType("BLOB");
b.Property<string>("EntityKey")
.IsRequired()
.HasColumnType("TEXT");
b.Property<long>("Version")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.ToTable("LightningChannels");
b.ToTable("LightningChannels", t =>
{
t.HasTrigger("LC_TRIGGER_AFTER_DELETE_CHANNEL");
t.HasTrigger("LC_TRIGGER_AFTER_INSERT_CHANNEL");
t.HasTrigger("LC_TRIGGER_AFTER_UPDATE_CHANNEL");
});
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.\"EntityKey\", \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.\"EntityKey\", \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\n INSERT INTO \"OutboxItems\" (\"Entity\", \"Version\", \"Key\", \"ActionType\") SELECT 'Channel', \r\n OLD.\"Version\" + 1, \r\n NEW.\"EntityKey\", \r\n 1;\r\nEND;");
});
modelBuilder.Entity("BTCPayApp.Core.Data.ChannelAlias", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<string>("ChannelId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Type")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("ChannelId");
b.ToTable("ChannelAliases");
});
modelBuilder.Entity("BTCPayApp.Core.Data.Outbox", b =>
{
b.Property<string>("Entity")
.HasColumnType("TEXT");
b.Property<string>("Key")
.HasColumnType("TEXT");
b.Property<int>("ActionType")
.HasColumnType("INTEGER");
b.Property<long>("Version")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("Timestamp")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValueSql("datetime('now')");
b.HasKey("Entity", "Key", "ActionType", "Version");
b.ToTable("OutboxItems");
});
modelBuilder.Entity("BTCPayApp.Core.Data.Setting", b =>
@ -79,13 +158,51 @@ namespace BTCPayApp.Core.Migrations
b.Property<string>("Key")
.HasColumnType("TEXT");
b.Property<bool>("Backup")
.HasColumnType("INTEGER");
b.Property<string>("EntityKey")
.IsRequired()
.HasColumnType("TEXT");
b.Property<byte[]>("Value")
.IsRequired()
.HasColumnType("BLOB");
b.Property<long>("Version")
.HasColumnType("INTEGER");
b.HasKey("Key");
b.ToTable("Settings");
b.ToTable("Settings", t =>
{
t.HasTrigger("LC_TRIGGER_AFTER_DELETE_SETTING");
t.HasTrigger("LC_TRIGGER_AFTER_INSERT_SETTING");
t.HasTrigger("LC_TRIGGER_AFTER_UPDATE_SETTING");
});
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.\"EntityKey\", \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.\"EntityKey\", \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\nWHEN \r\n \r\n OLD.\"Backup\" IS TRUE\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.\"EntityKey\", \r\n 1;\r\nEND;");
});
modelBuilder.Entity("BTCPayApp.Core.Data.ChannelAlias", b =>
{
b.HasOne("BTCPayApp.Core.Data.Channel", "Channel")
.WithMany("Aliases")
.HasForeignKey("ChannelId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Channel");
});
modelBuilder.Entity("BTCPayApp.Core.Data.Channel", b =>
{
b.Navigation("Aliases");
});
#pragma warning restore 612, 618
}

View File

@ -1,10 +1,14 @@
using BTCPayApp.CommonServer;
using System.Xml.Linq;
using BTCPayApp.CommonServer;
using BTCPayApp.Core.Attempt2;
using BTCPayApp.Core.Auth;
using BTCPayApp.Core.Contracts;
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;
@ -20,10 +24,12 @@ public static class StartupExtensions
{
var dir = provider.GetRequiredService<IDataDirectoryProvider>().GetAppDataDirectory().ConfigureAwait(false).GetAwaiter().GetResult();
options.UseSqlite($"Data Source={dir}/app.db");
options.UseSqlLiteTriggers();
});
serviceCollection.AddHostedService<AppDatabaseMigrator>();
serviceCollection.AddHttpClient();
serviceCollection.AddSingleton<BTCPayConnectionManager>();
serviceCollection.AddSingleton<SyncService>();
serviceCollection.AddSingleton<LightningNodeManager>();
serviceCollection.AddSingleton<OnChainWalletManager>();
serviceCollection.AddSingleton<BTCPayAppServerClient>();
@ -37,12 +43,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;

View File

@ -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;

View File

@ -19,23 +19,23 @@ class BTCPayAppTestServer : BaseWebApplicationFactory<Program>
{
if (newDir)
{
_config.AddOrReplace("BTCPAYAPP_DIRNAME", "btcpayserver-test-" + RandomUtils.GetUInt32());
Config.AddOrReplace("BTCPAYAPP_DIRNAME", "btcpayserver-test-" + RandomUtils.GetUInt32());
}
}
}
class BaseWebApplicationFactory<T> : WebApplicationFactory<T> where T : class
{
protected IHost? _host;
protected readonly ITestOutputHelper _output;
protected readonly Dictionary<string, string> _config;
protected readonly Task _playwrightInstallTask;
protected IHost? Host;
protected readonly ITestOutputHelper Output;
protected readonly Dictionary<string, string> Config;
protected readonly Task PlaywrightInstallTask;
public string ServerAddress
{
get
{
if (_host is null)
if (Host is null)
{
CreateDefaultClient();
}
@ -46,12 +46,12 @@ class BaseWebApplicationFactory<T> : WebApplicationFactory<T> where T : class
public BaseWebApplicationFactory(ITestOutputHelper output, Dictionary<string, string>? config = null)
{
_output = output;
Output = output;
_config = config ?? new();
Config = config ?? new();
_playwrightInstallTask ??= Task.Run(InstallPlaywright);
PlaywrightInstallTask ??= Task.Run(InstallPlaywright);
}
public class LifetimeBridge
@ -76,9 +76,9 @@ class BaseWebApplicationFactory<T> : WebApplicationFactory<T> where T : class
TaskCompletionSource<string[]> tcs = new TaskCompletionSource<string[]>();
builder.ConfigureWebHost(webHostBuilder => webHostBuilder.UseKestrel().UseUrls("https://127.0.0.1:0").ConfigureServices(collection => collection.AddSingleton<LifetimeBridge>(provider => new LifetimeBridge(provider.GetRequiredService<IHostApplicationLifetime>(), provider.GetRequiredService<IServer>(), tcs))));
// configure and start the actual host using Kestrel.
_host = builder.Build();
_host.Start();
_host.Services.GetRequiredService<LifetimeBridge>();
Host = builder.Build();
Host.Start();
Host.Services.GetRequiredService<LifetimeBridge>();
// Extract the selected dynamic port out of the Kestrel server
// and assign it onto the client options for convenience so it
// "just works" as otherwise it'll be the default http://localhost
@ -96,7 +96,7 @@ class BaseWebApplicationFactory<T> : WebApplicationFactory<T> where T : class
base.ConfigureWebHost(builder);
builder
.ConfigureAppConfiguration(configurationBuilder =>
configurationBuilder.AddInMemoryCollection(_config))
configurationBuilder.AddInMemoryCollection(Config!))
.ConfigureLogging(
logging =>
{
@ -104,8 +104,8 @@ class BaseWebApplicationFactory<T> : WebApplicationFactory<T> where T : class
var useScopes = logging.UsesScopes();
// remove other logging providers, such as remote loggers or unnecessary event logs
logging.ClearProviders();
logging.Services.AddSingleton<ILoggerProvider>(r =>
new WebApplicationFactoryExtensions.XunitLoggerProvider(_output, useScopes));
logging.Services.AddSingleton<ILoggerProvider>(_ =>
new WebApplicationFactoryExtensions.XunitLoggerProvider(Output, useScopes));
});
}
@ -117,7 +117,7 @@ class BaseWebApplicationFactory<T> : WebApplicationFactory<T> where T : class
public async Task<IBrowserContext> InitializeAsync()
{
Assert.NotNull(ServerAddress);
await _playwrightInstallTask;
await PlaywrightInstallTask;
Playwright = await Microsoft.Playwright.Playwright.CreateAsync();
Browser = await Playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions
{

View File

@ -161,11 +161,8 @@ public static class WebApplicationFactoryExtensions
if (logging == default)
return false;
var includeScopes = logging?.GetValue("Console:IncludeScopes", false);
if (!includeScopes.Value)
includeScopes = logging?.GetValue("IncludeScopes", false);
return includeScopes.GetValueOrDefault(false);
var includeScopes = logging.GetValue("Console:IncludeScopes", false) || logging.GetValue("IncludeScopes", false);
return includeScopes;
}
#endregion

View File

@ -27,11 +27,15 @@
public SetupState SetupStateConnection() {
return State.Value.ConnectionState switch
{
HubConnectionState.Connected => SetupState.Completed,
HubConnectionState.Connecting => SetupState.Pending,
HubConnectionState.Reconnecting => SetupState.Pending,
HubConnectionState.Disconnected => SetupState.Pending,
_ => SetupState.Undetermined
BTCPayConnectionState.Init => SetupState.Pending,
BTCPayConnectionState.WaitingForAuth => SetupState.Pending,
BTCPayConnectionState.Connecting => SetupState.Pending,
BTCPayConnectionState.Syncing => SetupState.Pending,
BTCPayConnectionState.Disconnected => SetupState.Failed,
BTCPayConnectionState.ConnectedAsMaster => SetupState.Completed,
BTCPayConnectionState.ConnectedAsSlave => SetupState.Completed,
BTCPayConnectionState.ConnectedFinishedInitialSync => SetupState.Pending,
_ => SetupState.Undetermined
};
}

View File

@ -7,11 +7,11 @@ namespace BTCPayApp.UI.Features;
[FeatureState]
public record RootState
{
public HubConnectionState? ConnectionState;
public BTCPayConnectionState ConnectionState;
public OnChainWalletState? OnchainWalletState;
public LightningNodeState? LightningNodeState;
public record ConnectionStateUpdatedAction(HubConnectionState? State);
public record ConnectionStateUpdatedAction(BTCPayConnectionState State);
public record OnChainWalletStateUpdatedAction(OnChainWalletState State);
public record LightningNodeStateUpdatedAction(LightningNodeState State);

View File

@ -77,7 +77,7 @@
public async Task HandleValidSubmit()
{
_config!.Passcode = Model.NewPasscode;
await ConfigProvider.Set(BTCPayAppConfig.Key, _config);
await ConfigProvider.Set(BTCPayAppConfig.Key, _config, false);
NavigationManager.NavigateTo(Routes.Settings);
}

View File

@ -164,7 +164,7 @@
<ul class="list-group list-group-flush list-group-links">
<li class="list-group-item">
<div class="justify-content-start">
<span class="m-2 btcpay-status btcpay-status--@(State.Value.ConnectionState switch { HubConnectionState.Connected => "enabled", HubConnectionState.Disconnected => "disabled", _ => "pending" })"></span>
<span class="m-2 btcpay-status btcpay-status--@(State.Value.ConnectionState switch { BTCPayConnectionState.ConnectedAsMaster or BTCPayConnectionState.ConnectedAsSlave => "enabled", BTCPayConnectionState.Disconnected => "disabled", _ => "pending" })"></span>
<span>Hub: @State.Value.ConnectionState</span>
</div>
</li>
@ -209,7 +209,7 @@
if (HasPasscode)
{
_config!.Passcode = null;
await ConfigProvider.Set(BTCPayAppConfig.Key, _config);
await ConfigProvider.Set(BTCPayAppConfig.Key, _config, false);
}
}

View File

@ -263,7 +263,7 @@
}
}
private async void UpdatePeer(string toString, PeerInfo? value)
private async void UpdatePeer(string pubKeyHex, PeerInfo? value)
{
try
{
@ -271,7 +271,7 @@
await InvokeAsync(StateHasChanged);
await _semaphore.WaitAsync();
await Node.Peer(toString, value);
await Node.Peer(new PubKey(pubKeyHex), value);
}
finally
{

View File

@ -41,6 +41,16 @@
<TruncateCenter Text="@Node.NodeId.ToString()" Padding="15" Copy="true" Elastic="true" class="form-control-plaintext"/>
<label>Node ID</label>
</div>
<div class="form-floating">
<select class="form-select" @onchange="OnSelectLSP" value="@_config.JITLSP" >
<option value="">None</option>
@foreach (var jit in JITOptions)
{
<option value="@jit">@jit</option>
}
</select>
<label>JIT LSP</label>
</div>
}
@if (!string.IsNullOrEmpty(_config?.LightningDerivationPath))
@ -82,7 +92,7 @@
private string? ConfiguredConnectionString;
private string ConnectionString => LightningNodeManager.ConnectionString;
private string[] JITOptions;
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
@ -90,6 +100,7 @@
if (LightningNodeManager?.Node is not null)
{
_config = await LightningNodeManager.Node.GetConfig();
JITOptions = await LightningNodeManager.Node.GetJITLSPs();
}
var acc = AccountManager.GetAccount();
if (acc?.CurrentStoreId != null)
@ -151,4 +162,13 @@
Logger.LogError(ex, "Error configuring LN wallet");
}
}
private async Task OnSelectLSP(ChangeEventArgs obj)
{
_config = await LightningNodeManager.Node.GetConfig();
_config.JITLSP = obj.Value?.ToString();
await LightningNodeManager.Node.UpdateConfig(_config);
}
}

View File

@ -84,7 +84,7 @@
@code {
private bool CanConfigureWallet =>
State.Value.OnchainWalletState == OnChainWalletState.NotConfigured &&
State.Value.ConnectionState == HubConnectionState.Connected;
State.Value.ConnectionState == BTCPayConnectionState.ConnectedAsMaster;
private AppUserStoreInfo? Store => AccountManager.GetCurrentStore();
private string? _storePaymentMethodIdentifier;

View File

@ -3,10 +3,9 @@
@using BTCPayApp.UI.Features
@using BTCPayApp.Core.Attempt2
@using BTCPayApp.UI.Components.Layout
@using BTCPayServer.Lightning
@using NBitcoin
@using LightningPayment = BTCPayApp.CommonServer.Models.LightningPayment
@using BTCPayApp.Core.Data
@using BTCPayServer.Lightning
@using NBitcoin.Crypto
@inject LightningNodeManager LightningNodeManager
@inject IState<RootState> State
@ -22,88 +21,87 @@
@if (State.Value.LightningNodeState is LightningNodeState.Loaded)
{
<article class="my-4">
<h3>Payments</h3>
@if (_payments?.Any() is true)
{
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th></th>
<th>Payment Hash</th>
<th>Inbound</th>
<th>Id</th>
<th>Preimage</th>
<th>Secret</th>
<th>Timestamp</th>
<th>Value</th>
<th>Status</th>
<th>Invoices</th>
</tr>
</thead>
<tbody>
@foreach (var payment in _payments)
{
<tr>
<td>
@if (payment.Status == LightningPaymentStatus.Pending)
{
<button type="button" class="btn btn-primary btn-sm" @onclick="() => Cancel( payment.Inbound? payment.PaymentHash:payment.PaymentId, payment.Inbound)">Cancel</button>
}
</td>
<td>@payment.PaymentHash</td>
<td>@payment.Inbound</td>
<td>@payment.PaymentId</td>
<td>@payment.Preimage</td>
<td>@payment.Secret</td>
<td>@payment.Timestamp</td>
<td>@payment.Value</td>
<td>@payment.Status</td>
<td>@string.Join('\n', payment.PaymentRequests)</td>
</tr>
}
</tbody>
</table>
</div>
}
else
{
<p class="text-muted">There are no payments, yet.</p>
}
<div>
<div class="input-group">
<input type="number" class="form-control" min="0" @bind="paymentRequestAmt" placeholder="sats"/>
<button type="button" class=" btn btn-primary" @onclick="ReceivePayment">Receive payment</button>
</div>
<div class="input-group">
<input type="number" class="form-control" placeholder="explicit amount in sats, optional" min="0" @bind="paymentRequestAmt"/>
<input type="text"class="form-control" placeholder="bolt11" @bind="paymentRequestSend"/>
<button type="button" class=" btn btn-primary" @onclick="SendPayment">Send payment</button>
</div>
@if (paymentResponse is not null)
<h3>Payments</h3>
@if (_payments?.Any() is true)
{
<p>@paymentResponse</p>
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th></th>
<th>Payment Hash</th>
<th>Inbound</th>
<th>Id</th>
<th>Preimage</th>
<th>Secret</th>
<th>Timestamp</th>
<th>Value</th>
<th>Status</th>
<th>Invoices</th>
</tr>
</thead>
<tbody>
@foreach (var payment in _payments)
{
<tr>
<td>
@if (payment.Status == LightningPaymentStatus.Pending)
{
<button type="button" class="btn btn-primary btn-sm" @onclick="() => Cancel(payment)">Cancel</button>
}
</td>
<td>@payment.PaymentHash</td>
<td>@payment.Inbound</td>
<td>@payment.PaymentId</td>
<td>@payment.Preimage</td>
<td>@payment.Secret</td>
<td>@payment.Timestamp</td>
<td>@payment.Value</td>
<td>@payment.Status</td>
<td>@payment.PaymentRequest</td>
</tr>
}
</tbody>
</table>
</div>
}
</div>
</article>
<button class="btn btn-outline-primary" type="button" @onclick="FetchData">Refresh data</button>
} else if (Loading)
else
{
<p class="text-muted">There are no payments, yet.</p>
}
<div>
<div class="input-group">
<input type="number" class="form-control" min="0" @bind="paymentRequestAmt" placeholder="sats"/>
<button type="button" class=" btn btn-primary" @onclick="ReceivePayment">Receive payment</button>
</div>
<div class="input-group">
<input type="number" class="form-control" placeholder="explicit amount in sats, optional" min="0" @bind="paymentRequestAmt"/>
<input type="text"class="form-control" placeholder="bolt11" @bind="paymentRequestSend"/>
<button type="button" class=" btn btn-primary" @onclick="SendPayment">Send payment</button>
</div>
@if (paymentResponse is not null)
{
<p>@paymentResponse</p>
}
</div>
</article>
<button class="btn btn-outline-primary" type="button" @onclick="FetchData">Refresh data</button>
}
else if (Loading)
{
<span>loading...</span>
}
</section>
@code {
private string _nodeId;
private LightningConfig? _config;
private List<LightningPayment?>? _payments;
private List<AppLightningPayment>? _payments;
private LDKNode? Node => LightningNodeManager.Node;
private decimal? paymentRequestAmt;
private string? paymentRequestSend;
@ -119,77 +117,54 @@
private async Task FetchData()
{
Loading = true;
await InvokeAsync(StateHasChanged);
try
await Wrap(async () =>
{
await _semaphore.WaitAsync();
_config = await Node.GetConfig();
_nodeId = Node.NodeId.ToString();
_payments = await LightningNodeManager.Node?.PaymentsManager.List(payments => payments);
}
finally
{
Loading = false;
await InvokeAsync(StateHasChanged);
_semaphore.Release();
}
if (Node is null) return;
_payments = await Node.PaymentsManager.List(payments => payments);
});
}
private async void ReceivePayment()
{
if (Loading || paymentRequestAmt is null) return;
try
if (paymentRequestSend is null) return;
await Wrap(async () =>
{
Loading = true;
await InvokeAsync(StateHasChanged);
await _semaphore.WaitAsync();
var hash = new uint256(Hashes.SHA256(RandomUtils.GetBytes(32)));
var result = await Node.PaymentsManager.RequestPayment(LightMoney.Satoshis(paymentRequestAmt??0), TimeSpan.FromDays(1), hash);
try
{
var hash = new uint256(Hashes.SHA256(RandomUtils.GetBytes(32)));
var result = await Node.PaymentsManager.RequestPayment(LightMoney.Satoshis(paymentRequestAmt ?? 0), TimeSpan.FromDays(1), hash);
paymentResponse = $"Payment request created with invs {string.Join(',',result.PaymentRequests)}";
paymentRequestAmt = null;
}
catch (Exception e)
{
paymentResponse = $"Error: {e.Message}";
}
finally
{
Loading = false;
await InvokeAsync(StateHasChanged);
_semaphore.Release();
}
paymentResponse = $"Payment request created: {result.PaymentRequest}";
paymentRequestAmt = null;
}
catch (Exception e)
{
paymentResponse = $"Error: {e.Message}";
}
});
}
private async void SendPayment()
{
if (Loading || paymentRequestSend is null) return;
try
if (paymentRequestSend is null) return;
await Wrap(async () =>
{
Loading = true;
await InvokeAsync(StateHasChanged);
await _semaphore.WaitAsync();
var invoice = BOLT11PaymentRequest.Parse(paymentRequestSend, Node.Network );
var result = await Node.PaymentsManager.PayInvoice(invoice, paymentRequestAmt is null? null: LightMoney.Satoshis((long)paymentRequestAmt.Value));
paymentResponse = $"Payment {result.PaymentId} sent with status {result.Status}";
paymentRequestAmt = null;
paymentRequestSend = null;
}
catch (Exception e)
{
paymentResponse = $"Error: {e.Message}";
}
finally
{
Loading = false;
await InvokeAsync(StateHasChanged);
_semaphore.Release();
}
try
{
var invoice = BOLT11PaymentRequest.Parse(paymentRequestSend, Node.Network);
var result = await Node.PaymentsManager.PayInvoice(invoice, paymentRequestAmt is null ? null : LightMoney.Satoshis((long) paymentRequestAmt.Value));
paymentResponse = $"Payment {result.PaymentId} sent with status {result.Status}";
paymentRequestAmt = null;
paymentRequestSend = null;
}
catch (Exception e)
{
paymentResponse = $"Error: {e.Message}";
}
});
}
private async void Cancel(string paymentId, bool inb)
private async Task Wrap(Func<Task> action)
{
if (Loading) return;
try
@ -197,7 +172,7 @@
Loading = true;
await InvokeAsync(StateHasChanged);
await _semaphore.WaitAsync();
await Node.PaymentsManager.Cancel(paymentId, inb);
await action();
}
finally
{
@ -206,4 +181,13 @@
_semaphore.Release();
}
}
}
private async Task Cancel(AppLightningPayment payment)
{
await Wrap(async () =>
{
await Node.PaymentsManager.Cancel(payment);
});
}
}

View File

@ -101,7 +101,7 @@
private bool CanConfigureWallet =>
State.Value.OnchainWalletState == OnChainWalletState.NotConfigured &&
State.Value.ConnectionState == HubConnectionState.Connected;
State.Value.ConnectionState == BTCPayConnectionState.ConnectedAsMaster;
protected override async Task OnInitializedAsync()
{

View File

@ -37,7 +37,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));

View File

@ -10,55 +10,60 @@ 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;
_httpClient = httpClient ?? new HttpClient();
}
public async Task<GetObjectResponse> GetObjectAsync(GetObjectRequest request)
public async Task<GetObjectResponse> GetObjectAsync(GetObjectRequest request, CancellationToken cancellationToken = default)
{
var url = new Uri(_endpoint, GET_OBJECT);
return await SendRequestAsync<GetObjectRequest, GetObjectResponse>(request, url);
return await SendRequestAsync<GetObjectRequest, GetObjectResponse>(request, url, cancellationToken);
}
public async Task<PutObjectResponse> PutObjectAsync(PutObjectRequest request)
public async Task<PutObjectResponse> PutObjectAsync(PutObjectRequest request, CancellationToken cancellationToken = default)
{
var url = new Uri(_endpoint, PUT_OBJECTS);
return await SendRequestAsync<PutObjectRequest, PutObjectResponse>(request, url);
return await SendRequestAsync<PutObjectRequest, PutObjectResponse>(request, url, cancellationToken);
}
public async Task<DeleteObjectResponse> DeleteObjectAsync(DeleteObjectRequest request)
public async Task<DeleteObjectResponse> DeleteObjectAsync(DeleteObjectRequest request, CancellationToken cancellationToken = default)
{
var url = new Uri(_endpoint, DELETE_OBJECT);
return await SendRequestAsync<DeleteObjectRequest, DeleteObjectResponse>(request, url);
return await SendRequestAsync<DeleteObjectRequest, DeleteObjectResponse>(request, url, cancellationToken);
}
public async Task<ListKeyVersionsResponse> ListKeyVersionsAsync(ListKeyVersionsRequest request)
public async Task<ListKeyVersionsResponse> ListKeyVersionsAsync(ListKeyVersionsRequest request, CancellationToken cancellationToken = default)
{
var url = new Uri(_endpoint, LIST_KEY_VERSIONS);
return await SendRequestAsync<ListKeyVersionsRequest, ListKeyVersionsResponse>(request, url);
return await SendRequestAsync<ListKeyVersionsRequest, ListKeyVersionsResponse>(request, url, cancellationToken);
}
private async Task<TResponse> SendRequestAsync<TRequest, TResponse>(TRequest request, Uri url)
private async Task<TResponse> SendRequestAsync<TRequest, TResponse>(TRequest request, Uri url, CancellationToken cancellationToken)
where TRequest : IMessage<TRequest>
where TResponse : IMessage<TResponse>, new()
{
var requestContent = new ByteArrayContent(request.ToByteArray());
requestContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/octet-stream");
foreach (var (key, value) in _httpClient.DefaultRequestHeaders)
{
requestContent.Headers.TryAddWithoutValidation(key, value);
}
var response = await _httpClient.PostAsync(url, requestContent);
var response = await _httpClient.PostAsync(url, requestContent, cancellationToken);
if (!response.IsSuccessStatusCode)
{
var errorContent = await response.Content.ReadAsStringAsync();
var errorContent = await response.Content.ReadAsStringAsync(cancellationToken);
throw new VssClientException($"HTTP error {(int)response.StatusCode} occurred: {errorContent}");
}
var responseBytes = await response.Content.ReadAsByteArrayAsync();
var responseBytes = await response.Content.ReadAsByteArrayAsync(cancellationToken);
var parsedResponse = (TResponse)new TResponse().Descriptor.Parser.ParseFrom(responseBytes);
if (parsedResponse is GetObjectResponse {Value: null})

View File

@ -13,26 +13,26 @@ public interface IVSSAPI
/// </summary>
/// <param name="request">The request details including the key to fetch.</param>
/// <returns>A task representing the asynchronous operation, which encapsulates the response with the fetched value.</returns>
Task<GetObjectResponse> GetObjectAsync(GetObjectRequest request);
Task<GetObjectResponse> GetObjectAsync(GetObjectRequest request, CancellationToken cancellationToken = default);
/// <summary>
/// Asynchronously writes objects as part of a single transaction to the VSS.
/// </summary>
/// <param name="request">The details of objects to be put, encapsulated as a transaction.</param>
/// <returns>A task representing the asynchronous operation, which encapsulates the response after putting objects.</returns>
Task<PutObjectResponse> PutObjectAsync(PutObjectRequest request);
Task<PutObjectResponse> PutObjectAsync(PutObjectRequest request, CancellationToken cancellationToken = default);
/// <summary>
/// Asynchronously deletes a specified object from the VSS.
/// </summary>
/// <param name="request">The details of the object to delete, including key and value.</param>
/// <returns>A task representing the asynchronous operation, which encapsulates the response after deleting the object.</returns>
Task<DeleteObjectResponse> DeleteObjectAsync(DeleteObjectRequest request);
Task<DeleteObjectResponse> DeleteObjectAsync(DeleteObjectRequest request, CancellationToken cancellationToken = default);
/// <summary>
/// Asynchronously lists all keys and their corresponding versions based on a specified store ID.
/// </summary>
/// <param name="request">The request details including the store ID for which keys and versions need to be listed.</param>
/// <returns>A task representing the asynchronous operation, which encapsulates the response with all listed keys and versions.</returns>
Task<ListKeyVersionsResponse> ListKeyVersionsAsync(ListKeyVersionsRequest request);
Task<ListKeyVersionsResponse> ListKeyVersionsAsync(ListKeyVersionsRequest request, CancellationToken cancellationToken = default);
}

View File

@ -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;
}
}

View File

@ -4,6 +4,7 @@ using VSSProto;
namespace BTCPayApp.VSS;
public class VSSApiEncryptorClient: IVSSAPI
{
private readonly IVSSAPI _vssApi;
@ -15,9 +16,9 @@ public class VSSApiEncryptorClient: IVSSAPI
_encryptor = encryptor;
}
public async Task<GetObjectResponse> GetObjectAsync(GetObjectRequest request)
public async Task<GetObjectResponse> GetObjectAsync(GetObjectRequest request, CancellationToken cancellationToken = default)
{
var response = await _vssApi.GetObjectAsync(request);
var response = await _vssApi.GetObjectAsync(request, cancellationToken);
if (response.Value?.Value is null)
{
return response;
@ -28,7 +29,7 @@ public class VSSApiEncryptorClient: IVSSAPI
return response;
}
public async Task<PutObjectResponse> PutObjectAsync(PutObjectRequest request)
public async Task<PutObjectResponse> PutObjectAsync(PutObjectRequest request, CancellationToken cancellationToken = default)
{
var newReq = request.Clone();
foreach (var obj in newReq.TransactionItems)
@ -38,19 +39,19 @@ public class VSSApiEncryptorClient: IVSSAPI
var encryptedValue = _encryptor.Protect(obj.Value.ToByteArray());
obj.Value = ByteString.CopyFrom(encryptedValue);
}
return await _vssApi.PutObjectAsync(newReq);
return await _vssApi.PutObjectAsync(newReq, cancellationToken);
}
public Task<DeleteObjectResponse> DeleteObjectAsync(DeleteObjectRequest request)
public Task<DeleteObjectResponse> DeleteObjectAsync(DeleteObjectRequest request, CancellationToken cancellationToken = default)
{
return _vssApi.DeleteObjectAsync(request);
return _vssApi.DeleteObjectAsync(request, cancellationToken);
}
public async Task<ListKeyVersionsResponse> ListKeyVersionsAsync(ListKeyVersionsRequest request)
public async Task<ListKeyVersionsResponse> ListKeyVersionsAsync(ListKeyVersionsRequest request, CancellationToken cancellationToken = default)
{
var x = await _vssApi.ListKeyVersionsAsync(request);
var x = await _vssApi.ListKeyVersionsAsync(request, cancellationToken);
foreach (var keyVersion in x.KeyVersions)
{

@ -1 +1 @@
Subproject commit 5b7dafe1426acc03d7c6e13a98b1c5a760f893d2
Subproject commit 7bdb136c276f53a4c24b01d98f8bcb2de6f52254