Compare commits
18 Commits
master
...
jit-backup
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b4dea11bc6 | ||
|
|
f06cf66e81 | ||
|
|
e6ee0d8a2f | ||
|
|
53fe9def9d | ||
|
|
12ef00719e | ||
|
|
9b7a2ff7a2 | ||
|
|
b4896685cb | ||
|
|
9c09a440e2 | ||
|
|
ec215938df | ||
|
|
a05d039a55 | ||
|
|
f7fe33a730 | ||
|
|
e02ca2ba78 | ||
|
|
ee3a83c235 | ||
|
|
4526824fb4 | ||
|
|
9953264836 | ||
|
|
c1788faefe | ||
|
|
86c699d823 | ||
|
|
a75e6b9929 |
46
BTCPayApp.Core/Attempt2/AppToServerHelper.cs
Normal file
46
BTCPayApp.Core/Attempt2/AppToServerHelper.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
13
BTCPayApp.Core/Attempt2/BTCPayConnectionState.cs
Normal file
13
BTCPayApp.Core/Attempt2/BTCPayConnectionState.cs
Normal file
@ -0,0 +1,13 @@
|
||||
namespace BTCPayApp.Core.Attempt2;
|
||||
|
||||
public enum BTCPayConnectionState
|
||||
{
|
||||
Init,
|
||||
WaitingForAuth,
|
||||
Connecting,
|
||||
Syncing,
|
||||
Disconnected,
|
||||
ConnectedAsMaster,
|
||||
ConnectedAsSlave,
|
||||
ConnectedFinishedInitialSync
|
||||
}
|
||||
@ -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;
|
||||
|
||||
32
BTCPayApp.Core/Attempt2/ConfigHelpers.cs
Normal file
32
BTCPayApp.Core/Attempt2/ConfigHelpers.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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());
|
||||
}
|
||||
|
||||
@ -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)
|
||||
{
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
{
|
||||
|
||||
59
BTCPayApp.Core/Attempt2/SingleKeyDataProtector.cs
Normal file
59
BTCPayApp.Core/Attempt2/SingleKeyDataProtector.cs
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
289
BTCPayApp.Core/Attempt2/SyncService.cs
Normal file
289
BTCPayApp.Core/Attempt2/SyncService.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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" />
|
||||
|
||||
@ -3,6 +3,6 @@
|
||||
public interface IConfigProvider
|
||||
{
|
||||
Task<T?> Get<T>(string key);
|
||||
Task Set<T>(string key, T? value);
|
||||
Task Set<T>(string key, T? value, bool backup);
|
||||
Task<IEnumerable<string>> List(string prefix);
|
||||
}
|
||||
@ -1,3 +1,8 @@
|
||||
namespace BTCPayApp.Core.Contracts;
|
||||
|
||||
public interface ISecureConfigProvider : IConfigProvider;
|
||||
public interface ISecureConfigProvider
|
||||
|
||||
{
|
||||
Task<T?> Get<T>(string key);
|
||||
Task Set<T>(string key, T? value);
|
||||
}
|
||||
@ -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; }
|
||||
}
|
||||
41
BTCPayApp.Core/Data/AppLightningPayment.cs
Normal file
41
BTCPayApp.Core/Data/AppLightningPayment.cs
Normal 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 { }
|
||||
}
|
||||
}
|
||||
@ -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; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
23
BTCPayApp.Core/Data/EFExtensions.cs
Normal file
23
BTCPayApp.Core/Data/EFExtensions.cs
Normal 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);
|
||||
// }
|
||||
}
|
||||
}
|
||||
@ -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; }
|
||||
}
|
||||
10
BTCPayApp.Core/Data/Outbox.cs
Normal file
10
BTCPayApp.Core/Data/Outbox.cs
Normal 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; }
|
||||
}
|
||||
8
BTCPayApp.Core/Data/OutboxAction.cs
Normal file
8
BTCPayApp.Core/Data/OutboxAction.cs
Normal file
@ -0,0 +1,8 @@
|
||||
namespace BTCPayApp.Core.Data;
|
||||
|
||||
public enum OutboxAction
|
||||
{
|
||||
Insert,
|
||||
Update,
|
||||
Delete
|
||||
}
|
||||
8
BTCPayApp.Core/Data/PeerInfo.cs
Normal file
8
BTCPayApp.Core/Data/PeerInfo.cs
Normal 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; }
|
||||
}
|
||||
30
BTCPayApp.Core/Data/RemoteToLocalSyncService.cs
Normal file
30
BTCPayApp.Core/Data/RemoteToLocalSyncService.cs
Normal 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
|
||||
|
||||
|
||||
|
||||
}
|
||||
@ -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 { }
|
||||
}
|
||||
}
|
||||
|
||||
43
BTCPayApp.Core/Data/TLVHelper.cs
Normal file
43
BTCPayApp.Core/Data/TLVHelper.cs
Normal file
@ -0,0 +1,43 @@
|
||||
namespace BTCPayApp.Core.Data;
|
||||
|
||||
public static class TLVHelper
|
||||
{
|
||||
public record TLV(byte Tag, byte[] Value);
|
||||
|
||||
public static byte[] Write(List<TLV> tlvList)
|
||||
{
|
||||
List<byte> byteArray = new List<byte>();
|
||||
|
||||
foreach (var tlv in tlvList)
|
||||
{
|
||||
byteArray.Add(tlv.Tag);
|
||||
byteArray.AddRange(BitConverter.GetBytes(tlv.Value.Length));
|
||||
byteArray.AddRange(tlv.Value);
|
||||
}
|
||||
|
||||
return byteArray.ToArray();
|
||||
}
|
||||
|
||||
public static List<TLV> Read(byte[] byteArray)
|
||||
{
|
||||
var tlvList = new List<TLV>();
|
||||
var index = 0;
|
||||
|
||||
while (index < byteArray.Length)
|
||||
{
|
||||
var tag = byteArray[index];
|
||||
index += 1;
|
||||
|
||||
var length = BitConverter.ToInt32(byteArray, index);
|
||||
index += 4;
|
||||
|
||||
var value = new byte[length];
|
||||
Array.Copy(byteArray, index, value, 0, length);
|
||||
index += length;
|
||||
|
||||
tlvList.Add(new TLV(tag, value));
|
||||
}
|
||||
|
||||
return tlvList;
|
||||
}
|
||||
}
|
||||
36
BTCPayApp.Core/Data/ValueConversionExtensions.cs
Normal file
36
BTCPayApp.Core/Data/ValueConversionExtensions.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
11
BTCPayApp.Core/Data/VersionedData.cs
Normal file
11
BTCPayApp.Core/Data/VersionedData.cs
Normal 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; }
|
||||
}
|
||||
@ -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);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -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))
|
||||
{
|
||||
|
||||
116
BTCPayApp.Core/JsonConverters/UInt256JsonConverter.cs
Normal file
116
BTCPayApp.Core/JsonConverters/UInt256JsonConverter.cs
Normal 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());
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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(() =>
|
||||
{
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
203
BTCPayApp.Core/LSP/JIT/Flow2Client.cs
Normal file
203
BTCPayApp.Core/LSP/JIT/Flow2Client.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
21
BTCPayApp.Core/LSP/JIT/FlowFeeRequest.cs
Normal file
21
BTCPayApp.Core/LSP/JIT/FlowFeeRequest.cs
Normal 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; }
|
||||
}
|
||||
9
BTCPayApp.Core/LSP/JIT/FlowFeeResponse.cs
Normal file
9
BTCPayApp.Core/LSP/JIT/FlowFeeResponse.cs
Normal 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; }
|
||||
}
|
||||
31
BTCPayApp.Core/LSP/JIT/FlowInfoResponse.cs
Normal file
31
BTCPayApp.Core/LSP/JIT/FlowInfoResponse.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
18
BTCPayApp.Core/LSP/JIT/FlowProposalRequest.cs
Normal file
18
BTCPayApp.Core/LSP/JIT/FlowProposalRequest.cs
Normal 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; }
|
||||
}
|
||||
8
BTCPayApp.Core/LSP/JIT/FlowProposalResponse.cs
Normal file
8
BTCPayApp.Core/LSP/JIT/FlowProposalResponse.cs
Normal 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; }
|
||||
}
|
||||
48
BTCPayApp.Core/LSP/JIT/IJITService.cs
Normal file
48
BTCPayApp.Core/LSP/JIT/IJITService.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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" });
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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(
|
||||
213
BTCPayApp.Core/Migrations/20240726135741_triggers.Designer.cs
generated
Normal file
213
BTCPayApp.Core/Migrations/20240726135741_triggers.Designer.cs
generated
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
176
BTCPayApp.Core/Migrations/20240726135741_triggers.cs
Normal file
176
BTCPayApp.Core/Migrations/20240726135741_triggers.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
{
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
{
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@ -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()
|
||||
{
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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})
|
||||
|
||||
@ -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);
|
||||
}
|
||||
@ -18,9 +18,9 @@ public class ConcurrentMultiDictionary<TKey, TValue>
|
||||
|
||||
private readonly ConcurrentDictionary<TKey, Bag> _dictionary;
|
||||
|
||||
public ConcurrentMultiDictionary()
|
||||
public ConcurrentMultiDictionary(IEqualityComparer<TKey> invariantCultureIgnoreCase = null)
|
||||
{
|
||||
_dictionary = new ConcurrentDictionary<TKey, Bag>();
|
||||
_dictionary = new ConcurrentDictionary<TKey, Bag>(invariantCultureIgnoreCase);
|
||||
}
|
||||
|
||||
public int Count => _dictionary.Count;
|
||||
@ -41,7 +41,7 @@ public class ConcurrentMultiDictionary<TKey, TValue>
|
||||
|
||||
public bool AddOrReplace(TKey key, TValue value)
|
||||
{
|
||||
Remove(key, value);
|
||||
Remove(key, value, out _);
|
||||
return Add(key, value);
|
||||
}
|
||||
|
||||
@ -60,8 +60,9 @@ public class ConcurrentMultiDictionary<TKey, TValue>
|
||||
items = null;
|
||||
return false;
|
||||
}
|
||||
public bool Remove(TKey key, TValue value)
|
||||
public bool Remove(TKey key, TValue value, out bool keyRemoved)
|
||||
{
|
||||
keyRemoved = false;
|
||||
var spinWait = new SpinWait();
|
||||
while (true)
|
||||
{
|
||||
@ -82,7 +83,7 @@ public class ConcurrentMultiDictionary<TKey, TValue>
|
||||
}
|
||||
}
|
||||
if (spinAndRetry) { spinWait.SpinOnce(); continue; }
|
||||
bool keyRemoved = _dictionary.TryRemove(key, out var currentBag);
|
||||
keyRemoved = _dictionary.TryRemove(key, out var currentBag);
|
||||
Debug.Assert(keyRemoved, $"Key {key} was not removed");
|
||||
Debug.Assert(bag == currentBag, $"Removed wrong bag");
|
||||
return true;
|
||||
@ -134,6 +135,11 @@ public class ConcurrentMultiDictionary<TKey, TValue>
|
||||
{
|
||||
return _dictionary.Keys.Any(key => Contains(key, value));
|
||||
}
|
||||
|
||||
public bool ContainsValue(TKey key, TValue value)
|
||||
{
|
||||
return Contains(key, value);
|
||||
}
|
||||
public IEnumerable<TKey> GetKeysContainingValue(IEnumerable<TValue> value)
|
||||
{
|
||||
return _dictionary.Keys.Where(key => Contains(key, value));
|
||||
@ -142,4 +148,20 @@ public class ConcurrentMultiDictionary<TKey, TValue>
|
||||
{
|
||||
return _dictionary.Keys.Where(key => Contains(key, value));
|
||||
}
|
||||
|
||||
public bool RemoveValue(TValue value, out TKey[] keysRemoved)
|
||||
{
|
||||
List<TKey> keys = [];
|
||||
var anyValueRemoved = false;
|
||||
foreach (var key in GetKeysContainingValue(value))
|
||||
{
|
||||
anyValueRemoved = Remove(key, value, out var keyRemoved) || anyValueRemoved;
|
||||
if (keyRemoved)
|
||||
{
|
||||
keys.Add(key);
|
||||
}
|
||||
}
|
||||
keysRemoved = keys.ToArray();
|
||||
return anyValueRemoved;
|
||||
}
|
||||
}
|
||||
@ -4,6 +4,7 @@ using VSSProto;
|
||||
|
||||
namespace BTCPayApp.VSS;
|
||||
|
||||
|
||||
public class VSSApiEncryptorClient: IVSSAPI
|
||||
{
|
||||
private readonly IVSSAPI _vssApi;
|
||||
@ -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
|
||||
Loading…
Reference in New Issue
Block a user