Compare commits
No commits in common. "master" and "send_logs" have entirely different histories.
10
.github/workflows/build-test.yml
vendored
10
.github/workflows/build-test.yml
vendored
@ -7,7 +7,7 @@ on:
|
||||
- '**/*.md'
|
||||
- '**/*.gitignore'
|
||||
- '**/*.gitattributes'
|
||||
pull_request_target:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
@ -36,9 +36,7 @@ jobs:
|
||||
run: dotnet build --configuration Release BTCPayApp.Server
|
||||
# Setup infrastructure
|
||||
- name: Start containers
|
||||
run: |
|
||||
docker compose -f "submodules/btcpayserver/BTCPayServer.Tests/docker-compose.yml" build
|
||||
docker compose -f "submodules/btcpayserver/BTCPayServer.Tests/docker-compose.yml" up -d dev
|
||||
run: docker compose -f "submodules/btcpayserver/BTCPayServer.Tests/docker-compose.yml" up -d dev
|
||||
- name: Start BTCPay
|
||||
run: |
|
||||
./setup.sh
|
||||
@ -67,9 +65,6 @@ jobs:
|
||||
dotnet-version: 8.0.x
|
||||
- name: Install workloads
|
||||
run: dotnet workload install maui
|
||||
- name: Clean before build
|
||||
run: |
|
||||
dotnet clean BTCPayApp.Maui/BTCPayApp.Maui.csproj
|
||||
- name: Build
|
||||
# TODO: Use proper keystore once we switch to real releases
|
||||
# https://learn.microsoft.com/en-us/dotnet/maui/android/deployment/publish-cli?view=net-maui-8.0#code-try-4
|
||||
@ -83,7 +78,6 @@ jobs:
|
||||
name: org.btcpayserver.BTCPayApp-Signed.apk
|
||||
path: publish/android/org.btcpayserver.BTCPayApp-Signed.apk
|
||||
- name: Create pre-release
|
||||
if: success() && github.ref == 'refs/heads/master'
|
||||
uses: marvinpinto/action-automatic-releases@latest
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@ -55,11 +55,11 @@ public class AuthStateProvider(
|
||||
}
|
||||
}
|
||||
|
||||
public BTCPayAppClient GetClient(string? baseUri = null, string? token = null)
|
||||
public BTCPayAppClient GetClient(string? baseUri = null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(baseUri) && string.IsNullOrEmpty(Account?.BaseUri))
|
||||
throw new ArgumentException("No base URI present or provided.", nameof(baseUri));
|
||||
token ??= Account?.ModeToken ?? Account?.OwnerToken;
|
||||
var token = Account?.ModeToken ?? Account?.OwnerToken;
|
||||
return new BTCPayAppClient(baseUri ?? Account!.BaseUri, token, clientFactory.CreateClient());
|
||||
}
|
||||
|
||||
@ -199,7 +199,7 @@ public class AuthStateProvider(
|
||||
await CheckAuthenticated(true);
|
||||
store = GetUserStore(store.Id)!;
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch (Exception)
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
@ -243,19 +243,6 @@ public class AuthStateProvider(
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<FormResult<LoginInfoResult>> LoginInfo(string serverUrl, string email, CancellationToken? cancellation = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await GetClient(serverUrl).LoginInfo(email, cancellation.GetValueOrDefault());
|
||||
return new FormResult<LoginInfoResult>(true, string.Empty, response);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return new FormResult<LoginInfoResult>(false, e.Message, null);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<FormResult> Login(string serverUrl, string email, string password, string? otp = null, CancellationToken? cancellation = default)
|
||||
{
|
||||
var payload = new LoginRequest
|
||||
@ -278,19 +265,13 @@ public class AuthStateProvider(
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<FormResult> LoginWithCode(string serverUrl, string? email, string code, CancellationToken? cancellation = default)
|
||||
public async Task<FormResult> LoginWithCode(string serverUrl, string email, string code, CancellationToken? cancellation = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var client = GetClient(serverUrl);
|
||||
var response = await client.Login(code, cancellation.GetValueOrDefault());
|
||||
if (string.IsNullOrEmpty(response.AccessToken)) throw new Exception("Did not obtain valid API token.");
|
||||
if (string.IsNullOrEmpty(email))
|
||||
{
|
||||
var clientWithToken = GetClient(serverUrl, response.AccessToken);
|
||||
var userInfo = await clientWithToken.GetUserInfo();
|
||||
email = userInfo?.Email!;
|
||||
}
|
||||
var account = new BTCPayAccount(serverUrl, email, response.AccessToken);
|
||||
await SetAccount(account);
|
||||
return new FormResult(true);
|
||||
|
||||
@ -9,16 +9,15 @@ public interface IAccountManager
|
||||
public BTCPayAccount? Account { get; }
|
||||
public AppUserInfo? UserInfo { get; }
|
||||
public AppUserStoreInfo? CurrentStore { get; }
|
||||
public BTCPayAppClient GetClient(string? baseUri = null, string? token = null);
|
||||
public BTCPayAppClient GetClient(string? baseUri = null);
|
||||
public Task<string?> GetEncryptionKey();
|
||||
public Task SetEncryptionKey(string value);
|
||||
public Task<bool> CheckAuthenticated(bool refreshUser = false);
|
||||
public Task<bool> IsAuthorized(string policy, object? resource = null);
|
||||
public Task<FormResult> AddAccountWithEncyptionKey(string serverUrl, string email, string key);
|
||||
public Task<FormResult<AcceptInviteResult>> AcceptInvite(string inviteUrl, CancellationToken? cancellation = default);
|
||||
public Task<FormResult<LoginInfoResult>> LoginInfo(string serverUrl, string email, CancellationToken? cancellation = default);
|
||||
public Task<FormResult> Login(string serverUrl, string email, string password, string? otp, CancellationToken? cancellation = default);
|
||||
public Task<FormResult> LoginWithCode(string serverUrl, string? email, string code, CancellationToken? cancellation = default);
|
||||
public Task<FormResult> LoginWithCode(string serverUrl, string email, string code, CancellationToken? cancellation = default);
|
||||
public Task<FormResult> Register(string serverUrl, string email, string password, CancellationToken? cancellation = default);
|
||||
public Task<FormResult> ResetPassword(string serverUrl, string email, string? resetCode, string? newPassword, CancellationToken? cancellation = default);
|
||||
public Task<FormResult<ApplicationUserData>> ChangePassword(string currentPassword, string newPassword, CancellationToken? cancellation = default);
|
||||
|
||||
@ -25,13 +25,13 @@
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.1" />
|
||||
<PackageReference Include="NBitcoin" Version="8.0.13" />
|
||||
<PackageReference Include="NBitcoin" Version="8.0.8" />
|
||||
<PackageReference Include="Microsoft.Extensions.Identity.Core" Version="8.0.11" />
|
||||
<PackageReference Include="org.ldk" Version="0.1.2" />
|
||||
<PackageReference Include="Serilog.Extensions.Hosting" Version="8.0.0" />
|
||||
<PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.SQLite.Maui" Version="1.9.7" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
|
||||
<PackageReference Include="TypedSignalR.Client" Version="3.6.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
||||
@ -37,12 +37,6 @@ public class BTCPayAppClient(string baseUri, string? apiKey = null, HttpClient?
|
||||
return await SendHttpRequest<JObject>("btcpayapp/register", payload, HttpMethod.Post, cancellation);
|
||||
}
|
||||
|
||||
public async Task<LoginInfoResult> LoginInfo(string email, CancellationToken cancellation = default)
|
||||
{
|
||||
var payload = new Dictionary<string, object> { { "email", email } };
|
||||
return await SendHttpRequest<LoginInfoResult>("btcpayapp/login-info", payload, HttpMethod.Get, cancellation);
|
||||
}
|
||||
|
||||
public async Task<AuthenticationResponse> Login(LoginRequest payload, CancellationToken cancellation = default)
|
||||
{
|
||||
return await SendHttpRequest<AuthenticationResponse>("btcpayapp/login", payload, HttpMethod.Post, cancellation);
|
||||
@ -70,20 +64,16 @@ public class BTCPayAppClient(string baseUri, string? apiKey = null, HttpClient?
|
||||
return await SendHttpRequest<JObject?>(path, payload, HttpMethod.Post, cancellation);
|
||||
}
|
||||
|
||||
public async Task<JObject?> CreatePosInvoice(Models.CreatePosInvoiceRequest req, CancellationToken cancellation = default)
|
||||
public async Task<JObject?> CreatePosInvoice(CreatePosInvoiceRequest req, CancellationToken cancellation = default)
|
||||
{
|
||||
var query = new Dictionary<string, object>();
|
||||
if (req.Total != null) query.Add("amount", req.Total.Value.ToString(CultureInfo.InvariantCulture));
|
||||
if (req.DiscountPercent != null) query.Add("discount", req.DiscountPercent.Value.ToString(CultureInfo.InvariantCulture));
|
||||
if (req.Tip != null) query.Add("tip", req.Tip.Value.ToString(CultureInfo.InvariantCulture));
|
||||
if (req.PosData != null) query.Add("posData", req.PosData);
|
||||
return await SendHttpRequest<JObject?>($"apps/{req.AppId}/pos/light", query, HttpMethod.Post, cancellation);
|
||||
}
|
||||
|
||||
public async Task<string> SubmitLNURLWithdrawForInvoice(SubmitLnUrlRequest req, CancellationToken cancellation = default)
|
||||
{
|
||||
return await SendHttpRequest<string>($"plugins/NFC", req, HttpMethod.Post, cancellation);
|
||||
}
|
||||
|
||||
public virtual async Task<T> UploadFileRequest<T>(string apiPath, StreamContent fileContent, string fileName, string mimeType, CancellationToken token = default)
|
||||
{
|
||||
using MultipartFormDataContent multipartContent = new();
|
||||
|
||||
@ -220,17 +220,12 @@ public class BTCPayAppServerClient(ILogger<BTCPayAppServerClient> _logger, IServ
|
||||
.Select(channel => channel.channelDetails)
|
||||
.OfType<ChannelDetails>()
|
||||
.ToArray();
|
||||
var balances = Node.ClaimableBalances;
|
||||
var closing = balances
|
||||
.Where(b => b is Balance.Balance_ClaimableAwaitingConfirmations)
|
||||
.ToArray();
|
||||
return new LightningNodeBalance
|
||||
{
|
||||
OffchainBalance = new OffchainBalance
|
||||
{
|
||||
Local = LightMoney.MilliSatoshis(channels.Sum(channel => channel.get_outbound_capacity_msat())),
|
||||
Remote = LightMoney.MilliSatoshis(channels.Sum(channel => channel.get_inbound_capacity_msat())),
|
||||
Closing = LightMoney.Satoshis(closing.Sum(balance => balance.claimable_amount_satoshis()))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@ -38,7 +38,6 @@ public class BTCPayConnectionManager(
|
||||
public Network? ReportedNetwork { get; private set; }
|
||||
public string? ReportedNodeInfo { get; set; }
|
||||
private bool ForceSlaveMode { get; set; }
|
||||
public bool RunningInBackground { get; set; }
|
||||
|
||||
public event AsyncEventHandler<(BTCPayConnectionState Old, BTCPayConnectionState New)>? ConnectionChanged;
|
||||
|
||||
@ -53,7 +52,7 @@ public class BTCPayConnectionManager(
|
||||
if (_connectionState == value) return;
|
||||
var old = _connectionState;
|
||||
_connectionState = value;
|
||||
logger.LogInformation("Connection state changed{BgInfo}: {Old} -> {ConnectionState}", BgInfo, old, _connectionState);
|
||||
logger.LogInformation("Connection state changed: {Old} -> {ConnectionState}", old, _connectionState);
|
||||
ConnectionChanged?.Invoke(this, (old, _connectionState));
|
||||
}
|
||||
finally
|
||||
@ -86,17 +85,17 @@ public class BTCPayConnectionManager(
|
||||
var deviceId = await configProvider.GetDeviceIdentifier();
|
||||
if (masterId is null && ConnectionState == BTCPayConnectionState.ConnectedAsSecondary && !ForceSlaveMode)
|
||||
{
|
||||
logger.LogInformation("OnMasterUpdated{BgInfo}: Syncing slave {DeviceId}", BgInfo, deviceId);
|
||||
logger.LogInformation("OnMasterUpdated: Syncing slave {DeviceId}", deviceId);
|
||||
ConnectionState = BTCPayConnectionState.Syncing;
|
||||
}
|
||||
else if (deviceId == masterId)
|
||||
{
|
||||
logger.LogInformation("OnMasterUpdated{BgInfo}: Setting master to {DeviceId}", BgInfo, deviceId);
|
||||
logger.LogInformation("OnMasterUpdated: Setting master to {DeviceId}", deviceId);
|
||||
ConnectionState = BTCPayConnectionState.ConnectedAsPrimary;
|
||||
}
|
||||
else if (ConnectionState == BTCPayConnectionState.ConnectedAsPrimary && masterId != deviceId)
|
||||
{
|
||||
logger.LogInformation("OnMasterUpdated{BgInfo}: New master {MasterId} - Device: {DeviceId}", BgInfo, masterId, deviceId);
|
||||
logger.LogInformation("OnMasterUpdated: New master {MasterId} - Device: {DeviceId}", masterId, deviceId);
|
||||
ConnectionState = BTCPayConnectionState.Syncing;
|
||||
}
|
||||
}, _cts.Token);
|
||||
@ -174,13 +173,13 @@ public class BTCPayConnectionManager(
|
||||
catch (HttpRequestException ex) when (ex.StatusCode is HttpStatusCode.Unauthorized)
|
||||
{
|
||||
await accountManager.Logout();
|
||||
logger.LogInformation("Signed out user because of unauthorized response{BgInfo}", BgInfo);
|
||||
logger.LogInformation("Signed out user because of unauthorized response");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await Task.Delay(500);
|
||||
if (ex is not TaskCanceledException)
|
||||
logger.LogError("Error while connecting to hub{BgInfo}: {Message}", BgInfo, ex.Message);
|
||||
logger.LogError($"Error while connecting to hub: {ex.Message}");
|
||||
}
|
||||
}
|
||||
Connection = connection;
|
||||
@ -205,12 +204,12 @@ public class BTCPayConnectionManager(
|
||||
var masterDevice = await HubProxy!.GetCurrentMaster();
|
||||
if (deviceIdentifier == masterDevice)
|
||||
{
|
||||
logger.LogInformation("Syncing master to remote{BgInfo}: {DeviceId}", BgInfo, deviceIdentifier);
|
||||
logger.LogInformation("Syncing master to remote: {DeviceId}", deviceIdentifier);
|
||||
await syncService.SyncToRemote(CancellationToken.None);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogInformation("Syncing to local{BgInfo}. Master: {MasterId} - Device: {DeviceId}", BgInfo, masterDevice, deviceIdentifier);
|
||||
logger.LogInformation("Syncing to local. Master: {MasterId} - Device: {DeviceId}", masterDevice, deviceIdentifier);
|
||||
await syncService.SyncToLocal();
|
||||
}
|
||||
newState = BTCPayConnectionState.ConnectedFinishedInitialSync;
|
||||
@ -247,12 +246,12 @@ public class BTCPayConnectionManager(
|
||||
}
|
||||
catch (System.Security.Cryptography.CryptographicException ex) when (newState is BTCPayConnectionState.Syncing or BTCPayConnectionState.Connecting)
|
||||
{
|
||||
logger.LogError(ex, "Error while changing connection state from {Old} to {New}{BgInfo}", e.Old, e.New, BgInfo);
|
||||
logger.LogError(ex, "Error while changing connection state from {Old} to {New}", e.Old, e.New);
|
||||
newState = BTCPayConnectionState.WaitingForEncryptionKey;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error while changing connection state from {Old} to {New}{BgInfo}", e.Old, e.New, BgInfo);
|
||||
logger.LogError(ex, "Error while changing connection state from {Old} to {New}", e.Old, e.New);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
@ -269,7 +268,7 @@ public class BTCPayConnectionManager(
|
||||
|
||||
private Task OnNotifyServerEvent(object? sender, ServerEvent e)
|
||||
{
|
||||
logger.LogInformation("OnNotifyServerEvent{BgInfo}: {Type} - {Details}", BgInfo, e.Type, e.ToString());
|
||||
logger.LogInformation("OnNotifyServerEvent: {Type} - {Details}", e.Type, e.ToString());
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
@ -298,7 +297,7 @@ public class BTCPayConnectionManager(
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogError(e, "Error while handling authentication state change{BgInfo}", BgInfo);
|
||||
logger.LogError(e, "Error while handling authentication state change");
|
||||
}
|
||||
}, _cts.Token);
|
||||
}
|
||||
@ -307,7 +306,7 @@ public class BTCPayConnectionManager(
|
||||
{
|
||||
if (Connection is not null)
|
||||
{
|
||||
logger.LogWarning("Killing connection{BgInfo}", BgInfo);
|
||||
logger.LogWarning("Killing connection");
|
||||
}
|
||||
var conn = Connection;
|
||||
Connection = null;
|
||||
@ -331,7 +330,7 @@ public class BTCPayConnectionManager(
|
||||
if (_connectionState == BTCPayConnectionState.ConnectedAsPrimary)
|
||||
{
|
||||
var deviceId = await configProvider.GetDeviceIdentifier();
|
||||
logger.LogInformation("Sending device master signal to turn off {DeviceId}{BgInfo}", deviceId, BgInfo);
|
||||
logger.LogInformation("Sending device master signal to turn off {DeviceId}", deviceId);
|
||||
await syncService.StopSync();
|
||||
await syncService.SyncToRemote(CancellationToken.None);
|
||||
if (HubProxy is not null)
|
||||
@ -349,7 +348,7 @@ public class BTCPayConnectionManager(
|
||||
|
||||
public Task OnClosed(Exception? ex)
|
||||
{
|
||||
logger.LogError("Hub connection closed{BgInfo}: {Message}", BgInfo, ex?.Message);
|
||||
logger.LogError("Hub connection closed: {Message}", ex?.Message);
|
||||
if (Connection?.State == HubConnectionState.Disconnected && ConnectionState != BTCPayConnectionState.Connecting)
|
||||
{
|
||||
ConnectionState = BTCPayConnectionState.Disconnected;
|
||||
@ -360,14 +359,14 @@ public class BTCPayConnectionManager(
|
||||
|
||||
public Task OnReconnected(string? connectionId)
|
||||
{
|
||||
logger.LogInformation("Hub connection reconnected{BgInfo}", BgInfo);
|
||||
logger.LogInformation("Hub connection reconnected");
|
||||
ConnectionState = BTCPayConnectionState.Syncing;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task OnReconnecting(Exception? ex)
|
||||
{
|
||||
logger.LogWarning("Hub connection reconnecting{BgInfo}: {Message}", BgInfo, ex?.Message);
|
||||
logger.LogWarning("Hub connection reconnecting: {Message}", ex?.Message);
|
||||
ConnectionState = BTCPayConnectionState.Connecting;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
@ -384,6 +383,4 @@ public class BTCPayConnectionManager(
|
||||
await HubProxy!.DeviceMasterSignal(deviceId, false);
|
||||
}
|
||||
}
|
||||
|
||||
private string BgInfo => RunningInBackground ? " (in background mode)" : string.Empty;
|
||||
}
|
||||
|
||||
@ -1,8 +1,5 @@
|
||||
using System.Text.Json.Serialization;
|
||||
namespace BTCPayApp.Core.BTCPayServer;
|
||||
|
||||
namespace BTCPayApp.Core.BTCPayServer;
|
||||
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public enum BTCPayConnectionState
|
||||
{
|
||||
Init,
|
||||
|
||||
@ -5,5 +5,4 @@ public static class Constants
|
||||
public const string LoginCodeSeparator = ";";
|
||||
public const string EncryptionKeySeparator = "*";
|
||||
public const string InviteSeparator = "/invite/";
|
||||
public const string POSQRLoginSeparator = "loginCode";
|
||||
}
|
||||
|
||||
@ -2,6 +2,6 @@
|
||||
{
|
||||
public interface IEmailService
|
||||
{
|
||||
Task SendAsync(string subject, string body, string recipient, string? attachFilePath = null);
|
||||
Task SendAsync(string subject, string body, string? recipient = null, string? attachFilePath = null);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,15 +0,0 @@
|
||||
using BTCPayApp.Core.Models;
|
||||
|
||||
namespace BTCPayApp.Core.Contracts;
|
||||
public interface INfcService: IDisposable
|
||||
{
|
||||
event EventHandler<NfcCardData> OnNfcDataReceived;
|
||||
void StartNfc();
|
||||
void EndNfc();
|
||||
}
|
||||
|
||||
public class NfcCardData
|
||||
{
|
||||
public string Message { get; set; }
|
||||
public byte[] Payload { get; set; }
|
||||
}
|
||||
@ -1,8 +1,5 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace BTCPayApp.Core.Contracts;
|
||||
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public enum SetupState
|
||||
{
|
||||
Undetermined,
|
||||
|
||||
@ -14,6 +14,7 @@ public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(op
|
||||
public DbSet<AppLightningPayment> LightningPayments { get; set; }
|
||||
public DbSet<Outbox> OutboxItems { get; set; }
|
||||
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<Outbox>()
|
||||
|
||||
@ -1,8 +0,0 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace BTCPayApp.Core.Data;
|
||||
|
||||
public class LogDbContext(DbContextOptions<LogDbContext> options) : DbContext(options)
|
||||
{
|
||||
public DbSet<LogEntry> Logs { get; set; }
|
||||
}
|
||||
@ -1,13 +0,0 @@
|
||||
using Serilog.Events;
|
||||
|
||||
namespace BTCPayApp.Core.Data;
|
||||
|
||||
public class LogEntry
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Level { get; set; }
|
||||
public DateTime TimeStamp { get; set; }
|
||||
public string RenderedMessage { get; set; }
|
||||
public string Exception { get; set; }
|
||||
public string Properties { get; set; }
|
||||
}
|
||||
@ -1,29 +1,40 @@
|
||||
using BTCPayApp.Core.Contracts;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Serilog;
|
||||
using Serilog.Core;
|
||||
using Serilog.Events;
|
||||
|
||||
namespace BTCPayApp.Core.Data;
|
||||
|
||||
public static class LoggingConfig
|
||||
{
|
||||
private const string OutputTemplate = "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} ({SourceContext}){NewLine}{Exception}";
|
||||
|
||||
public static void ConfigureLogging(IServiceCollection serviceCollection)
|
||||
{
|
||||
var serviceProvider = serviceCollection.BuildServiceProvider();
|
||||
var dirProvider = serviceProvider.GetRequiredService<IDataDirectoryProvider>();
|
||||
var appDir = dirProvider.GetAppDataDirectory().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
var dbPath = $"{appDir}/logs.db";
|
||||
var configProvider = serviceProvider.GetRequiredService<ConfigProvider>();
|
||||
var isDevEnv = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Development";
|
||||
var minLogLevel = isDevEnv ? LogEventLevel.Verbose : LogEventLevel.Information;
|
||||
|
||||
serviceCollection.AddSerilog();
|
||||
serviceCollection.AddDbContextFactory<LogDbContext>((_, options) =>
|
||||
var logLevel = isDevEnv ? LogEventLevel.Debug : LogEventLevel.Information;
|
||||
if (!isDevEnv)
|
||||
{
|
||||
options.UseSqlite($"Data Source={dbPath}");
|
||||
});
|
||||
try
|
||||
{
|
||||
var storedLevel = configProvider.Get<LogEventLevel?>("logLevel").ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
if (storedLevel is not null) logLevel = storedLevel.Value;
|
||||
}
|
||||
catch (Exception) { /* ignored */ }
|
||||
}
|
||||
|
||||
var levelSwitch = new LoggingLevelSwitch { MinimumLevel = logLevel };
|
||||
serviceCollection.AddSingleton(levelSwitch);
|
||||
serviceCollection.AddSerilog();
|
||||
|
||||
var outputTemplate = "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} ({SourceContext}){NewLine}{Exception}";
|
||||
var config = new LoggerConfiguration()
|
||||
.Enrich.FromLogContext()
|
||||
.WriteTo.Console(outputTemplate: outputTemplate) // Write to the console (optional)
|
||||
.MinimumLevel.ControlledBy(levelSwitch)
|
||||
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
|
||||
.MinimumLevel.Override("System", LogEventLevel.Warning);
|
||||
|
||||
/*
|
||||
"LDK": "Trace",
|
||||
@ -32,11 +43,22 @@ public static class LoggingConfig
|
||||
"LDK.BTCPayApp.Core.LDK.LDKPeerHandler": "Information",
|
||||
"LDK.lightning_background_processor": "Information"*/
|
||||
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.Enrich.FromLogContext()
|
||||
.WriteTo.SQLite(dbPath)
|
||||
.WriteTo.Console(outputTemplate: OutputTemplate, restrictedToMinimumLevel: minLogLevel)
|
||||
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
|
||||
.MinimumLevel.Override("System", LogEventLevel.Warning).CreateLogger();
|
||||
if (!isDevEnv)
|
||||
{
|
||||
var dirProvider = serviceProvider.GetRequiredService<IDataDirectoryProvider>();
|
||||
var appDir = dirProvider.GetAppDataDirectory().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
var logFilePath = Path.Combine(appDir, "logs", "btcpayapp-.log");
|
||||
config.WriteTo.File(
|
||||
logFilePath,
|
||||
buffered: true,
|
||||
fileSizeLimitBytes: 5 * 1024 * 1024, // 5 MB
|
||||
rollOnFileSizeLimit: true,
|
||||
rollingInterval: RollingInterval.Day,
|
||||
retainedFileCountLimit: 7,
|
||||
outputTemplate: outputTemplate
|
||||
);
|
||||
}
|
||||
|
||||
Log.Logger = config.CreateLogger();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,11 +1,8 @@
|
||||
using System.Text.Json.Serialization;
|
||||
namespace BTCPayApp.Core.Data;
|
||||
|
||||
namespace BTCPayApp.Core.Data;
|
||||
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public enum OutboxAction
|
||||
{
|
||||
Insert,
|
||||
Update,
|
||||
Delete
|
||||
}
|
||||
}
|
||||
@ -5,7 +5,6 @@ using BTCPayApp.Core.Contracts;
|
||||
using BTCPayApp.Core.Data;
|
||||
using BTCPayApp.Core.Helpers;
|
||||
using BTCPayApp.Core.LDK;
|
||||
using BTCPayApp.Core.Services;
|
||||
using BTCPayApp.Core.Wallet;
|
||||
using Laraue.EfCoreTriggers.SqlLite.Extensions;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@ -26,17 +25,16 @@ public static class StartupExtensions
|
||||
options.UseSqlite($"Data Source={dir}/app.db");
|
||||
options.UseSqlLiteTriggers();
|
||||
});
|
||||
serviceCollection.AddHostedService<AppDatabaseMigrator>();
|
||||
serviceCollection.AddSingleton<ConfigProvider, DatabaseConfigProvider>();
|
||||
|
||||
// Configure logging
|
||||
LoggingConfig.ConfigureLogging(serviceCollection);
|
||||
|
||||
serviceCollection.AddHostedService<AppDatabaseMigrator>();
|
||||
serviceCollection.AddSingleton<ConfigProvider, DatabaseConfigProvider>();
|
||||
serviceCollection.AddMemoryCache();
|
||||
serviceCollection.AddHttpClient();
|
||||
serviceCollection.AddSingleton<BTCPayConnectionManager>();
|
||||
serviceCollection.AddSingleton<SyncService>();
|
||||
serviceCollection.AddSingleton<LoggingService>();
|
||||
serviceCollection.AddSingleton<LightningNodeManager>();
|
||||
serviceCollection.AddSingleton<OnChainWalletManager>();
|
||||
serviceCollection.AddSingleton<BTCPayAppServerClient>();
|
||||
|
||||
@ -51,7 +51,7 @@ public static class StoreHelpers
|
||||
// lightning
|
||||
if (applyLighting && lightning is null && lightningNodeService is { IsActive: true, Node.ApiKeyManager: { } apiKeyManager })
|
||||
{
|
||||
var key = await apiKeyManager.GetKeyForStore(storeId, APIKeyPermission.Write);
|
||||
var key = await apiKeyManager.Create("Automated BTCPay Store Setup", APIKeyPermission.Write);
|
||||
lightning = await accountManager.GetClient().UpdateStorePaymentMethod(storeId,
|
||||
LightningNodeManager.PaymentMethodId, new UpdatePaymentMethodRequest
|
||||
{
|
||||
@ -72,7 +72,9 @@ public static class StoreHelpers
|
||||
if (jsonDoc.RootElement.TryGetProperty("accountDerivation", out var derivationSchemeElement) &&
|
||||
derivationSchemeElement.GetString() is { } derivationScheme &&
|
||||
config?.Derivations.Any(pair => pair.Value.Identifier == $"DERIVATIONSCHEME:{derivationScheme}") is true)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
@ -90,8 +92,10 @@ public static class StoreHelpers
|
||||
connectionStringElement.GetString() is { } connectionString &&
|
||||
LightningConnectionStringHelper.ExtractValues(connectionString, out var lnConnectionString) is { } lnValues &&
|
||||
lnConnectionString == "app" && lnValues.TryGetValue("key", out var key) && key is not null &&
|
||||
await node!.ApiKeyManager.CheckPermission(key, APIKeyPermission.Read))
|
||||
await node!.ApiKeyManager!.CheckPermission(key, APIKeyPermission.Read))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
@ -8,8 +8,8 @@
|
||||
/// <param name="Permission">Read or Write permissions, read implies being able to receive payments, write enables spending as well</param>
|
||||
public record APIKey(string Key, string Name, APIKeyPermission Permission)
|
||||
{
|
||||
public string ConnectionString(string userId)
|
||||
public string ConnectionString(string user)
|
||||
{
|
||||
return $"type=app;user={userId};key={Key}";
|
||||
return $"type=app;key={Key};user={user}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,10 +1,7 @@
|
||||
using System.Text.Json.Serialization;
|
||||
namespace BTCPayApp.Core.LDK;
|
||||
|
||||
namespace BTCPayApp.Core.LDK;
|
||||
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public enum APIKeyPermission
|
||||
{
|
||||
Read,
|
||||
Write,
|
||||
}
|
||||
}
|
||||
@ -3,7 +3,7 @@
|
||||
namespace BTCPayApp.Core.LDK;
|
||||
|
||||
/// <summary>
|
||||
/// A typed variant of <see cref="ILDKEventHandler"/> that handles a specific type of event
|
||||
/// A typed variant of <see cref="ILDKEventHandler"/> that handles a specific type of event
|
||||
/// </summary>
|
||||
/// <typeparam name="TEvent"></typeparam>
|
||||
public interface ILDKEventHandler<in TEvent>: ILDKEventHandler where TEvent : Event
|
||||
@ -18,10 +18,10 @@ public interface ILDKEventHandler
|
||||
Task Handle(Event @event)
|
||||
{
|
||||
var eventType = @event.GetType();
|
||||
|
||||
var result = GetType().GetInterfaces().FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(ILDKEventHandler<>) && i.GetGenericArguments()[0] == eventType)
|
||||
|
||||
var result = this.GetType().GetInterfaces().FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(ILDKEventHandler<>) && i.GetGenericArguments()[0] == eventType)
|
||||
?.GetMethod(nameof(ILDKEventHandler<Event>.Handle))
|
||||
?.Invoke(this, [@event]);
|
||||
?.Invoke(this, new object[] {@event});
|
||||
|
||||
if (result is Task task)
|
||||
return task;
|
||||
|
||||
@ -6,24 +6,44 @@ namespace BTCPayApp.Core.LDK;
|
||||
/// <summary>
|
||||
/// Runs the LDK background processor which handles the main event loop for the LDK library.
|
||||
/// </summary>
|
||||
public class LDKBackgroundProcessor(
|
||||
Persister persister,
|
||||
EventHandler eventHandler,
|
||||
ChainMonitor chainMonitor,
|
||||
ChannelManager channelManager,
|
||||
OnionMessenger onionMessenger,
|
||||
GossipSync gossipSync,
|
||||
PeerManager peerManager,
|
||||
Logger logger,
|
||||
WriteableScore scorer)
|
||||
: IScopedHostedService
|
||||
public class LDKBackgroundProcessor : IScopedHostedService
|
||||
{
|
||||
private readonly Persister _persister;
|
||||
private readonly EventHandler _eventHandler;
|
||||
private readonly ChainMonitor _chainMonitor;
|
||||
private readonly ChannelManager _channelManager;
|
||||
private readonly OnionMessenger _onionMessenger;
|
||||
private readonly GossipSync _gossipSync;
|
||||
private readonly PeerManager _peerManager;
|
||||
private readonly Logger _logger;
|
||||
private readonly WriteableScore _scorer;
|
||||
private BackgroundProcessor? _processor;
|
||||
|
||||
public LDKBackgroundProcessor(Persister persister,
|
||||
EventHandler eventHandler,
|
||||
ChainMonitor chainMonitor,
|
||||
ChannelManager channelManager,
|
||||
OnionMessenger onionMessenger,
|
||||
GossipSync gossipSync,
|
||||
PeerManager peerManager,
|
||||
Logger logger,
|
||||
WriteableScore scorer)
|
||||
{
|
||||
_persister = persister;
|
||||
_eventHandler = eventHandler;
|
||||
_chainMonitor = chainMonitor;
|
||||
_channelManager = channelManager;
|
||||
_onionMessenger = onionMessenger;
|
||||
_gossipSync = gossipSync;
|
||||
_peerManager = peerManager;
|
||||
_logger = logger;
|
||||
_scorer = scorer;
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await StopAsync(CancellationToken.None);
|
||||
_processor = BackgroundProcessor.start(persister, eventHandler, chainMonitor, channelManager, onionMessenger, gossipSync, peerManager, logger, Option_WriteableScoreZ.some(scorer));
|
||||
_processor = BackgroundProcessor.start(_persister, _eventHandler, _chainMonitor, _channelManager, _onionMessenger, _gossipSync, _peerManager, _logger, Option_WriteableScoreZ.some(_scorer));
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
|
||||
@ -7,19 +7,28 @@ namespace BTCPayApp.Core.LDK;
|
||||
/// <summary>
|
||||
/// Enables LDK to broadcast transactions through BTCPayServer.
|
||||
/// </summary>
|
||||
public class LDKBroadcaster(
|
||||
Network network,
|
||||
IEnumerable<IBroadcastGateKeeper> broadcastGateKeepers,
|
||||
OnChainWalletManager onChainWalletManager)
|
||||
: BroadcasterInterfaceInterface
|
||||
public class LDKBroadcaster : BroadcasterInterfaceInterface
|
||||
{
|
||||
private readonly Network _network;
|
||||
private readonly IEnumerable<IBroadcastGateKeeper> _broadcastGateKeepers;
|
||||
private readonly OnChainWalletManager _onChainWalletManager;
|
||||
|
||||
public LDKBroadcaster(
|
||||
Network network,
|
||||
IEnumerable<IBroadcastGateKeeper> broadcastGateKeepers, OnChainWalletManager onChainWalletManager)
|
||||
{
|
||||
_network = network;
|
||||
_broadcastGateKeepers = broadcastGateKeepers;
|
||||
_onChainWalletManager = onChainWalletManager;
|
||||
}
|
||||
|
||||
public void broadcast_transactions(byte[][] txs)
|
||||
{
|
||||
List<Task> tasks = new();
|
||||
foreach (var tx in txs)
|
||||
{
|
||||
var loadedTx = Transaction.Load(tx, network);
|
||||
if(broadcastGateKeepers.Any(gk => gk.DontBroadcast(loadedTx)))
|
||||
var loadedTx = Transaction.Load(tx, _network);
|
||||
if(_broadcastGateKeepers.Any(gk => gk.DontBroadcast(loadedTx)))
|
||||
continue;
|
||||
tasks.Add(Broadcast(loadedTx));
|
||||
}
|
||||
@ -28,7 +37,7 @@ public class LDKBroadcaster(
|
||||
|
||||
public async Task Broadcast(Transaction transaction, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await onChainWalletManager.BroadcastTransaction(transaction, cancellationToken);
|
||||
await _onChainWalletManager.BroadcastTransaction(transaction, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -2,11 +2,17 @@
|
||||
|
||||
namespace BTCPayApp.Core.LDK;
|
||||
|
||||
public class LDKBumpTransactionEventHandler(BumpTransactionEventHandler bumpTransactionEventHandler) : ILDKEventHandler<Event.Event_BumpTransaction>
|
||||
public class LDKBumpTransactionEventHandler: ILDKEventHandler<Event.Event_BumpTransaction>
|
||||
{
|
||||
private readonly BumpTransactionEventHandler _bumpTransactionEventHandler;
|
||||
|
||||
public LDKBumpTransactionEventHandler(BumpTransactionEventHandler bumpTransactionEventHandler)
|
||||
{
|
||||
_bumpTransactionEventHandler = bumpTransactionEventHandler;
|
||||
}
|
||||
public Task Handle(Event.Event_BumpTransaction @event)
|
||||
{
|
||||
bumpTransactionEventHandler.handle_event(@event.bump_transaction);
|
||||
_bumpTransactionEventHandler.handle_event(@event.bump_transaction);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -173,6 +173,7 @@ public class LDKChannelSync(
|
||||
private async Task OnTransactionUpdate(TransactionDetectedRequest txUpdate, CancellationToken cancellationToken)
|
||||
{
|
||||
logger.LogInformation("Transaction update {TxId}", txUpdate.TxId);
|
||||
|
||||
await PollForTransactionUpdates([new uint256(txUpdate.TxId)]);
|
||||
logger.LogInformation("Transaction update {TxId} processed", txUpdate.TxId);
|
||||
}
|
||||
|
||||
@ -4,8 +4,14 @@ using org.ldk.structs;
|
||||
|
||||
namespace BTCPayApp.Core.LDK;
|
||||
|
||||
public class LDKCoinSelector(OnChainWalletManager onChainWalletManager) : CoinSelectionSourceInterface
|
||||
public class LDKCoinSelector : CoinSelectionSourceInterface
|
||||
{
|
||||
private readonly OnChainWalletManager _onChainWalletManager;
|
||||
|
||||
public LDKCoinSelector(OnChainWalletManager onChainWalletManager)
|
||||
{
|
||||
_onChainWalletManager = onChainWalletManager;
|
||||
}
|
||||
public Result_CoinSelectionNoneZ select_confirmed_utxos(byte[] claim_id, Input[] must_spend,
|
||||
org.ldk.structs.TxOut[] must_pay_to,
|
||||
int target_feerate_sat_per_1000_weight)
|
||||
@ -15,7 +21,7 @@ public class LDKCoinSelector(OnChainWalletManager onChainWalletManager) : CoinSe
|
||||
var coins = must_spend.Select(x => x.Coin()).ToList();
|
||||
try
|
||||
{
|
||||
var tx = onChainWalletManager.CreateTransaction(
|
||||
var tx = _onChainWalletManager.CreateTransaction(
|
||||
txouts, feerate,
|
||||
coins).GetAwaiter().GetResult();
|
||||
|
||||
@ -32,7 +38,7 @@ public class LDKCoinSelector(OnChainWalletManager onChainWalletManager) : CoinSe
|
||||
|
||||
public Result_TransactionNoneZ sign_psbt(byte[] psbtBytes)
|
||||
{
|
||||
var signedPsbt = onChainWalletManager.SignTransaction(psbtBytes).GetAwaiter().GetResult();
|
||||
var signedPsbt = _onChainWalletManager.SignTransaction(psbtBytes).GetAwaiter().GetResult();
|
||||
return signedPsbt is null
|
||||
? Result_TransactionNoneZ.err()
|
||||
: Result_TransactionNoneZ.ok(signedPsbt.ExtractTransaction().ToBytes());
|
||||
|
||||
@ -3,12 +3,21 @@ using org.ldk.structs;
|
||||
|
||||
namespace BTCPayApp.Core.LDK;
|
||||
|
||||
public class LDKEventHandler(IEnumerable<ILDKEventHandler> eventHandlers, LDKWalletLogger ldkWalletLogger) : EventHandlerInterface
|
||||
public class LDKEventHandler : EventHandlerInterface
|
||||
{
|
||||
private readonly IEnumerable<ILDKEventHandler> _eventHandlers;
|
||||
private readonly LDKWalletLogger _ldkWalletLogger;
|
||||
|
||||
public LDKEventHandler(IEnumerable<ILDKEventHandler> eventHandlers, LDKWalletLogger ldkWalletLogger)
|
||||
{
|
||||
_eventHandlers = eventHandlers;
|
||||
_ldkWalletLogger = ldkWalletLogger;
|
||||
}
|
||||
|
||||
public Result_NoneReplayEventZ handle_event(Event @event)
|
||||
{
|
||||
ldkWalletLogger.LogInformation("Received event {Type}", @event.GetType());
|
||||
eventHandlers.AsParallel().ForAll(async void (handler) =>
|
||||
_ldkWalletLogger.LogInformation("Received event {Type}", @event.GetType());
|
||||
_eventHandlers.AsParallel().ForAll(async void (handler) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -16,7 +25,7 @@ public class LDKEventHandler(IEnumerable<ILDKEventHandler> eventHandlers, LDKWal
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ldkWalletLogger.LogError(ex, "Error handling event {EventType} with handler {HandlerType}", @event.GetType(), handler.GetType());
|
||||
_ldkWalletLogger.LogError(ex, "Error handling event {EventType} with handler {HandlerType}", @event.GetType(), handler.GetType());
|
||||
}
|
||||
});
|
||||
return Result_NoneReplayEventZ.ok();
|
||||
|
||||
@ -383,8 +383,9 @@ public static class LDKExtensions
|
||||
var reason = evt.reason.GetType().Name;
|
||||
switch (evt.reason)
|
||||
{
|
||||
|
||||
case ClosureReason.ClosureReason_CounterpartyForceClosed closureReasonCounterpartyForceClosed:
|
||||
reason += " with msg from peer: " + closureReasonCounterpartyForceClosed.peer_msg.get_a();
|
||||
reason += " with msg from peer: " +closureReasonCounterpartyForceClosed.peer_msg.get_a();
|
||||
break;
|
||||
case ClosureReason.ClosureReason_ProcessingError closureReasonProcessingError:
|
||||
reason += " " + closureReasonProcessingError.err;
|
||||
@ -393,6 +394,7 @@ public static class LDKExtensions
|
||||
return reason;
|
||||
}
|
||||
|
||||
|
||||
public static byte[]? GetPreimage(this PaymentPurpose purpose, out byte[]? secret)
|
||||
{
|
||||
switch (purpose)
|
||||
|
||||
@ -5,10 +5,18 @@ using Script = NBitcoin.Script;
|
||||
|
||||
namespace BTCPayApp.Core.LDK;
|
||||
|
||||
public class LDKFilter(LDKNode ldkNode, ConfigProvider configProvider) : FilterInterface
|
||||
public class LDKFilter : FilterInterface
|
||||
{
|
||||
private readonly LDKNode _ldkNode;
|
||||
private readonly ConfigProvider _configProvider;
|
||||
private readonly SemaphoreSlim _semaphore = new(1, 1);
|
||||
|
||||
public LDKFilter(LDKNode ldkNode, ConfigProvider configProvider)
|
||||
{
|
||||
_ldkNode = ldkNode;
|
||||
_configProvider = configProvider;
|
||||
}
|
||||
|
||||
public void register_tx(byte[] txid, byte[] script_pubkey)
|
||||
{
|
||||
var script = Script.FromBytesUnsafe(script_pubkey);
|
||||
@ -25,7 +33,7 @@ public class LDKFilter(LDKNode ldkNode, ConfigProvider configProvider) : FilterI
|
||||
|
||||
public async Task<List<LDKWatchedOutput>> GetWatchedOutputs()
|
||||
{
|
||||
return await GetWatchedOutputs(configProvider);
|
||||
return await GetWatchedOutputs(_configProvider);
|
||||
}
|
||||
|
||||
public static async Task<List<LDKWatchedOutput>> GetWatchedOutputs(ConfigProvider configProvider)
|
||||
@ -46,7 +54,7 @@ public class LDKFilter(LDKNode ldkNode, ConfigProvider configProvider) : FilterI
|
||||
}
|
||||
|
||||
watchedOutputs.Add(output);
|
||||
await configProvider.Set("ln:watchedOutputs", watchedOutputs, true);
|
||||
await _configProvider.Set("ln:watchedOutputs", watchedOutputs, true);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@ -54,15 +62,16 @@ public class LDKFilter(LDKNode ldkNode, ConfigProvider configProvider) : FilterI
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void Track(Script script)
|
||||
{
|
||||
_ = ldkNode.TrackScripts([script]);
|
||||
_ = _ldkNode.TrackScripts([script]);
|
||||
}
|
||||
|
||||
public async Task OutputsSpent(List<LDKWatchedOutput> spentWatchedOutputs)
|
||||
{
|
||||
var watchedOutputs = await GetWatchedOutputs();
|
||||
watchedOutputs.RemoveAll(w => spentWatchedOutputs.Any(s => s.Outpoint == w.Outpoint));
|
||||
await configProvider.Set("ln:watchedOutputs", watchedOutputs, true);
|
||||
await _configProvider.Set("ln:watchedOutputs", watchedOutputs, true);
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,8 +4,15 @@ using org.ldk.structs;
|
||||
|
||||
namespace BTCPayApp.Core.LDK;
|
||||
|
||||
public class LDKKVStore(ConfigProvider configProvider) : KVStoreInterface
|
||||
public class LDKKVStore:KVStoreInterface
|
||||
{
|
||||
private readonly ConfigProvider _configProvider;
|
||||
|
||||
public LDKKVStore(ConfigProvider configProvider)
|
||||
{
|
||||
_configProvider = configProvider;
|
||||
}
|
||||
|
||||
private string CombineKey(string primary_namespace, string secondary_namespace, string key)
|
||||
{
|
||||
var str = "ln:";
|
||||
@ -28,28 +35,28 @@ public class LDKKVStore(ConfigProvider configProvider) : KVStoreInterface
|
||||
public Result_CVec_u8ZIOErrorZ read(string primary_namespace, string secondary_namespace, string key)
|
||||
{
|
||||
var key1 = CombineKey(primary_namespace, secondary_namespace, key);
|
||||
var result = configProvider.Get<byte[]>(key1).ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
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 = CombineKey(primary_namespace, secondary_namespace, key);
|
||||
configProvider.Set(key1, buf, true).ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
_configProvider.Set(key1, buf, true).ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
return Result_NoneIOErrorZ.ok();
|
||||
}
|
||||
|
||||
public Result_NoneIOErrorZ remove(string primary_namespace, string secondary_namespace, string key, bool lazy)
|
||||
{
|
||||
var key1 = CombineKey(primary_namespace, secondary_namespace, key);
|
||||
configProvider.Set<byte[]>(key1, null, true).ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
_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 = CombineKey(primary_namespace, secondary_namespace, string.Empty);
|
||||
var result = configProvider.List(key1).ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
var result = _configProvider.List(key1).ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
return Result_CVec_StrZIOErrorZ.ok(result.ToArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -4,9 +4,16 @@ using org.ldk.structs;
|
||||
|
||||
namespace BTCPayApp.Core.LDK;
|
||||
|
||||
public class LDKLogger(ILoggerFactory loggerFactory) : LoggerInterface, ILogger
|
||||
public class LDKLogger : LoggerInterface, ILogger
|
||||
{
|
||||
private readonly ILogger _baseLogger = loggerFactory.CreateLogger("");
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
private readonly ILogger _baseLogger;
|
||||
|
||||
public LDKLogger(ILoggerFactory loggerFactory)
|
||||
{
|
||||
_loggerFactory = loggerFactory;
|
||||
_baseLogger = loggerFactory.CreateLogger("");
|
||||
}
|
||||
|
||||
public virtual void log(Record record)
|
||||
{
|
||||
@ -19,7 +26,7 @@ public class LDKLogger(ILoggerFactory loggerFactory) : LoggerInterface, ILogger
|
||||
Level.LDKLevel_Error => LogLevel.Error,
|
||||
Level.LDKLevel_Gossip => LogLevel.Trace,
|
||||
};
|
||||
loggerFactory.CreateLogger(record.get_module_path()).Log(level, "{Args}", record.get_args());
|
||||
_loggerFactory.CreateLogger(record.get_module_path()).Log(level, "{Args}", record.get_args());
|
||||
}
|
||||
|
||||
public IDisposable? BeginScope<TState>(TState state) where TState : notnull
|
||||
@ -37,4 +44,4 @@ public class LDKLogger(ILoggerFactory loggerFactory) : LoggerInterface, ILogger
|
||||
{
|
||||
_baseLogger.Log(logLevel, eventId, state, exception, formatter);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -27,10 +27,11 @@ public partial class LDKNode :
|
||||
return (await _memoryCache.GetOrCreateAsync(nameof(GetChannels), async entry =>
|
||||
{
|
||||
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
var dbChannels = dbContext.LightningChannels.AsNoTracking()
|
||||
.Include(channel => channel.Aliases).AsAsyncEnumerable();
|
||||
var channels = ServiceProvider.GetRequiredService<ChannelManager>().list_channels();
|
||||
|
||||
var result = new List<(Channel channel, ChannelDetails? channelDetails)>();
|
||||
await foreach (var dbChannel in dbChannels)
|
||||
{
|
||||
@ -48,7 +49,7 @@ public partial class LDKNode :
|
||||
public async Task Handle(Event.Event_ChannelClosed evt)
|
||||
{
|
||||
_logger.LogInformation("Channel {ChannelId} closed: {Reason}", Convert.ToHexString(evt.channel_id.get_a()).ToLowerInvariant(), evt.GetReason());
|
||||
await AddChannelData(evt.channel_id, new Dictionary<string, JsonElement>
|
||||
await AddChannelData(evt.channel_id, new Dictionary<string, JsonElement>()
|
||||
{
|
||||
{"CloseReason", JsonSerializer.SerializeToElement(evt.reason.write())},
|
||||
{"CloseReasonHuman", JsonSerializer.SerializeToElement(evt.GetReason())},
|
||||
@ -59,16 +60,17 @@ public partial class LDKNode :
|
||||
|
||||
public async Task Handle(Event.Event_ChannelPending evt)
|
||||
{
|
||||
await AddChannelData(evt.channel_id, new Dictionary<string, JsonElement>
|
||||
await AddChannelData(evt.channel_id, new Dictionary<string, JsonElement>()
|
||||
{
|
||||
{"PendingTimestamp", JsonSerializer.SerializeToElement(DateTimeOffset.UtcNow.ToUnixTimeSeconds())}
|
||||
});
|
||||
|
||||
_memoryCache.Remove(nameof(GetChannels));
|
||||
}
|
||||
|
||||
public async Task Handle(Event.Event_ChannelReady evt)
|
||||
{
|
||||
await AddChannelData(evt.channel_id, new Dictionary<string, JsonElement>
|
||||
await AddChannelData(evt.channel_id, new Dictionary<string, JsonElement>()
|
||||
{
|
||||
{"ReadyTimestamp", JsonSerializer.SerializeToElement(DateTimeOffset.UtcNow.ToUnixTimeSeconds())}
|
||||
});
|
||||
@ -84,7 +86,12 @@ public partial class LDKNode :
|
||||
}).WithCancellation(cancellationToken);
|
||||
}
|
||||
|
||||
public void InvalidateCache()
|
||||
public void PeersChanged()
|
||||
{
|
||||
_memoryCache.Remove(nameof(GetPeers));
|
||||
}
|
||||
|
||||
private void InvalidateCache()
|
||||
{
|
||||
_memoryCache.Remove(nameof(GetPeers));
|
||||
_memoryCache.Remove(nameof(GetChannels));
|
||||
@ -133,7 +140,10 @@ public partial class LDKNode :
|
||||
{
|
||||
var config = await GetConfig();
|
||||
var lsp = config.JITLSP;
|
||||
if (string.IsNullOrEmpty(lsp)) return null;
|
||||
if (lsp is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var jits = ServiceProvider.GetServices<IJITService>();
|
||||
return jits.FirstOrDefault(jit => jit.ProviderName == lsp);
|
||||
@ -195,20 +205,22 @@ public partial class LDKNode : IAsyncDisposable, IHostedService, IDisposable
|
||||
return;
|
||||
}
|
||||
|
||||
InvalidateCache();
|
||||
var walletConfig = await _onChainWalletManager.GetConfig();
|
||||
var lightningConfig = await _configProvider.Get<LightningConfig>(key: LightningConfig.Key) ?? new LightningConfig();
|
||||
|
||||
var keyPath = KeyPath.Parse(lightningConfig.LightningDerivationPath);
|
||||
|
||||
Seed = new Mnemonic(walletConfig.Mnemonic).DeriveExtKey().Derive(keyPath).PrivateKey.ToBytes();
|
||||
var services = ServiceProvider.GetServices<IScopedHostedService>();
|
||||
|
||||
_logger.LogInformation("Starting LDKNode services");
|
||||
var bb = await _onChainWalletManager.GetBestBlock();
|
||||
if (bb is null)
|
||||
{
|
||||
throw new InvalidOperationException("Best block could not be retrieved. Killing the startup");
|
||||
}
|
||||
|
||||
InvalidateCache();
|
||||
var walletConfig = await _onChainWalletManager.GetConfig();
|
||||
var lightningConfig = await _configProvider.Get<LightningConfig>(key: LightningConfig.Key) ?? new LightningConfig();
|
||||
var keyPath = KeyPath.Parse(lightningConfig.LightningDerivationPath);
|
||||
Seed = new Mnemonic(walletConfig.Mnemonic).DeriveExtKey().Derive(keyPath).PrivateKey.ToBytes();
|
||||
var services = ServiceProvider.GetServices<IScopedHostedService>();
|
||||
|
||||
_logger.LogInformation("Starting LDKNode services");
|
||||
foreach (var service in services)
|
||||
{
|
||||
_logger.LogInformation("Starting {Name}", service.GetType().Name);
|
||||
@ -272,11 +284,10 @@ public partial class LDKNode : IAsyncDisposable, IHostedService, IDisposable
|
||||
|
||||
public byte[] Seed { get; private set; }
|
||||
|
||||
public PaymentsManager PaymentsManager => ServiceProvider.GetRequiredService<PaymentsManager>();
|
||||
public LightningAPIKeyManager ApiKeyManager => ServiceProvider.GetRequiredService<LightningAPIKeyManager>();
|
||||
public PaymentsManager? PaymentsManager => ServiceProvider.GetRequiredService<PaymentsManager>();
|
||||
public LightningAPIKeyManager? ApiKeyManager => ServiceProvider.GetRequiredService<LightningAPIKeyManager>();
|
||||
public LDKPeerHandler PeerHandler => ServiceProvider.GetRequiredService<LDKPeerHandler>();
|
||||
public PubKey NodeId => new(ServiceProvider.GetRequiredService<ChannelManager>().get_our_node_id());
|
||||
public Balance[] ClaimableBalances => ServiceProvider.GetRequiredService<ChainMonitor>().get_claimable_balances([]);
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
@ -295,6 +306,7 @@ public partial class LDKNode : IAsyncDisposable, IHostedService, IDisposable
|
||||
return;
|
||||
// var identifier = _onChainWalletManager.WalletConfig.Derivations[WalletDerivation.LightningScripts].Identifier;
|
||||
|
||||
|
||||
_logger.LogInformation("Stopping LDKNode services");
|
||||
var services = ServiceProvider.GetServices<IScopedHostedService>();
|
||||
var tasks = services.Select(async service =>
|
||||
|
||||
@ -25,7 +25,6 @@ public class LDKPeerHandler(
|
||||
private CancellationTokenSource? _cts;
|
||||
private TaskCompletionSource? _configTcs;
|
||||
private readonly ObservableConcurrentDictionary<string, LDKTcpDescriptor> _descriptors = new();
|
||||
private readonly ConcurrentDictionary<string, Task<LDKTcpDescriptor?>> _connectionTasks = new();
|
||||
public EndPoint? Endpoint { get; set; }
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
@ -46,7 +45,7 @@ public class LDKPeerHandler(
|
||||
|
||||
private void DescriptorsOnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
node.InvalidateCache();
|
||||
node.PeersChanged();
|
||||
}
|
||||
|
||||
private async Task PeerBtcPayServerHost(object? sender, string? e)
|
||||
@ -81,6 +80,7 @@ public class LDKPeerHandler(
|
||||
var chans = await node.GetChannels(ctsToken);
|
||||
var channels = chans?.Where(pair => pair.channelDetails is not null)
|
||||
.Select(pair => pair.channelDetails!).ToList() ?? [];
|
||||
|
||||
var channelPeers = channels
|
||||
.Select(details => Convert.ToHexString(details.get_counterparty().get_node_id()).ToLower()).Distinct();
|
||||
var config = await node.GetConfig();
|
||||
@ -92,26 +92,22 @@ public class LDKPeerHandler(
|
||||
foreach (var persistentPeer in missingConnections)
|
||||
{
|
||||
var kv = config.Peers[persistentPeer];
|
||||
if (kv.Endpoint is not { } endpoint) continue;
|
||||
var nodeId = new PubKey(persistentPeer);
|
||||
var cts = CancellationTokenSource.CreateLinkedTokenSource(ctsToken);
|
||||
cts.CancelAfter(10000);
|
||||
tasks.Add(ConnectAsync(nodeId, endpoint, cts.Token));
|
||||
var nodeid = new PubKey(persistentPeer);
|
||||
if (kv.Endpoint is {} endpoint)
|
||||
{
|
||||
var cts = CancellationTokenSource.CreateLinkedTokenSource(ctsToken);
|
||||
cts.CancelAfter(10000);
|
||||
tasks.Add(ConnectAsync(nodeid, endpoint, cts.Token));
|
||||
}
|
||||
}
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
}
|
||||
catch (Exception e) when (e is { InnerException: SocketException })
|
||||
{
|
||||
_logger.LogError(e.Message);
|
||||
await Task.Delay(5000, ctsToken);
|
||||
}
|
||||
catch (Exception e) when (e is not OperationCanceledException)
|
||||
{
|
||||
_logger.LogError(e, "Error while attempting to connect to persistent peers");
|
||||
}
|
||||
finally
|
||||
{
|
||||
await Task.Delay(5000, ctsToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -157,7 +153,7 @@ public class LDKPeerHandler(
|
||||
using var listener = new TcpListener(new IPEndPoint(IPAddress.Any, 0));
|
||||
listener.Start();
|
||||
var ip = listener.LocalEndpoint;
|
||||
Endpoint = new IPEndPoint(IPAddress.Loopback, (int)ip.Port());
|
||||
Endpoint = new IPEndPoint(IPAddress.Loopback, (int) ip.Port());
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
var result = LDKTcpDescriptor.Inbound(peerManager,
|
||||
@ -181,28 +177,33 @@ public class LDKPeerHandler(
|
||||
throw new ArgumentException($"Invalid endpoint: {remote}", nameof(nodeInfo));
|
||||
}
|
||||
|
||||
private readonly ConcurrentDictionary<string, Task<LDKTcpDescriptor?>> _connectionTasks = new();
|
||||
|
||||
public async Task<LDKTcpDescriptor?> ConnectAsync(PubKey peerNodeId, PeerInfo peerInfo, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (peerInfo.Endpoint is not { } endpoint) return null;
|
||||
if (peerInfo.Label is not null)
|
||||
_logger.LogInformation("Attempting to connect to {NodeId} at {Endpoint} ({Label})", peerNodeId, endpoint.ToEndpointString(), peerInfo.Label);
|
||||
return await ConnectAsync(peerNodeId, endpoint, cancellationToken);
|
||||
if (peerInfo.Endpoint is {} endpoint)
|
||||
{
|
||||
if (peerInfo.Label is not null)
|
||||
_logger.LogInformation("Attempting to connect to {NodeId} at {Endpoint} ({Label})", peerNodeId, endpoint.ToEndpointString(), peerInfo.Label);
|
||||
return await ConnectAsync(peerNodeId, endpoint, cancellationToken);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task<LDKTcpDescriptor?> ConnectAsync(PubKey theirNodeId, EndPoint remote, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var nodeId = theirNodeId.ToString();
|
||||
//cache this task so that we don't have multiple attempts to connect to the same place
|
||||
if (_connectionTasks.TryGetValue(nodeId, out var task))
|
||||
if (_connectionTasks.TryGetValue(theirNodeId.ToString(), out var task))
|
||||
{
|
||||
_logger.LogInformation("Already attempting to connect to {NodeId}", nodeId);
|
||||
_logger.LogInformation("Already attempting to connect to {NodeId}", theirNodeId);
|
||||
return await task.WithCancellation(cancellationToken);
|
||||
}
|
||||
|
||||
var tcs = new TaskCompletionSource<LDKTcpDescriptor?>();
|
||||
try
|
||||
{
|
||||
if (!_connectionTasks.TryAdd(nodeId, tcs.Task))
|
||||
if (!_connectionTasks.TryAdd(theirNodeId.ToString(), tcs.Task))
|
||||
return null;
|
||||
|
||||
if (channelManager.get_our_node_id().SequenceEqual(theirNodeId.ToBytes()))
|
||||
@ -217,7 +218,7 @@ public class LDKPeerHandler(
|
||||
{
|
||||
var needsUpdate = false;
|
||||
var config = await node.GetConfig();
|
||||
if (!config.Peers.TryGetValue(nodeId, out var peer))
|
||||
if (!config.Peers.TryGetValue(theirNodeId.ToString(), out var peer))
|
||||
{
|
||||
peer = new PeerInfo { Trusted = true };
|
||||
needsUpdate = true;
|
||||
@ -234,25 +235,20 @@ public class LDKPeerHandler(
|
||||
}
|
||||
if (needsUpdate)
|
||||
await node.Peer(theirNodeId, peer);
|
||||
|
||||
_descriptors.TryAdd(result.Id, result);
|
||||
peerManager.process_events();
|
||||
await Task.Delay(TimeSpan.FromMilliseconds(250), cancellationToken);
|
||||
}
|
||||
|
||||
tcs.TrySetResult(result);
|
||||
}
|
||||
catch (SocketException e)
|
||||
{
|
||||
// wrap the original exception to report on the failing node URI
|
||||
tcs.TrySetException(new Exception($"Socket connection to {nodeId}@{remote} failed: {e.Message}", e));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
tcs.TrySetException(e);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_connectionTasks.TryRemove(nodeId, out _);
|
||||
_connectionTasks.TryRemove(theirNodeId.ToString(), out _);
|
||||
}
|
||||
|
||||
return await tcs.Task;
|
||||
@ -269,8 +265,6 @@ public class LDKPeerHandler(
|
||||
peer.Persistent = false;
|
||||
await node.Peer(id, peer);
|
||||
}
|
||||
|
||||
peerManager.process_events();
|
||||
_logger.LogInformation("Disconnected from {NodeId}", id);
|
||||
}
|
||||
}
|
||||
|
||||
@ -147,6 +147,7 @@ public class LDKPersistInterface : PersistInterface, IScopedHostedService
|
||||
if (otherId == fundingId)
|
||||
{
|
||||
otherId = null;
|
||||
|
||||
}
|
||||
if (otherId != null)
|
||||
{
|
||||
@ -193,7 +194,16 @@ public class LDKPersistInterface : PersistInterface, IScopedHostedService
|
||||
AsyncExtensions.RunInOtherThread(() =>
|
||||
_node.ArchiveChannel(ChannelId.v1_from_funding_outpoint(channelFundingOutpoint))).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
//
|
||||
// public async Task StartAsync(CancellationToken cancellationToken)
|
||||
// {
|
||||
// throw new NotImplementedException();
|
||||
// }
|
||||
//
|
||||
// public async Task StopAsync(CancellationToken cancellationToken)
|
||||
// {
|
||||
// throw new NotImplementedException();
|
||||
// }
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
|
||||
@ -2,23 +2,30 @@ using org.ldk.structs;
|
||||
|
||||
namespace BTCPayApp.Core.LDK;
|
||||
|
||||
public class LDKPersister(LDKNode ldkNode) : PersisterInterface
|
||||
public class LDKPersister : PersisterInterface
|
||||
{
|
||||
public Result_NoneIOErrorZ persist_manager(ChannelManager channelManager)
|
||||
private readonly LDKNode _ldkNode;
|
||||
|
||||
public LDKPersister(LDKNode ldkNode)
|
||||
{
|
||||
ldkNode.UpdateChannelManager(channelManager).ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
_ldkNode = ldkNode;
|
||||
}
|
||||
|
||||
public Result_NoneIOErrorZ persist_manager(ChannelManager channel_manager)
|
||||
{
|
||||
_ldkNode.UpdateChannelManager(channel_manager).ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
return Result_NoneIOErrorZ.ok();
|
||||
}
|
||||
|
||||
public Result_NoneIOErrorZ persist_graph(NetworkGraph networkGraph)
|
||||
public Result_NoneIOErrorZ persist_graph(NetworkGraph network_graph)
|
||||
{
|
||||
ldkNode.UpdateNetworkGraph(networkGraph).ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
_ldkNode.UpdateNetworkGraph(network_graph).ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
return Result_NoneIOErrorZ.ok();
|
||||
}
|
||||
|
||||
public Result_NoneIOErrorZ persist_scorer(WriteableScore scorer)
|
||||
{
|
||||
ldkNode.UpdateScore(scorer).ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
_ldkNode.UpdateScore(scorer).ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
return Result_NoneIOErrorZ.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -6,21 +6,32 @@ using org.ldk.structs;
|
||||
|
||||
namespace BTCPayApp.Core.LDK;
|
||||
|
||||
public class LDKRapidGossipSyncer(
|
||||
LDKNode ldkNode,
|
||||
RapidGossipSync rapidGossipSync,
|
||||
NetworkGraph networkGraph,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
ILogger<LDKRapidGossipSyncer> logger)
|
||||
: IScopedHostedService
|
||||
public class LDKRapidGossipSyncer : IScopedHostedService
|
||||
{
|
||||
private readonly LDKNode _ldkNode;
|
||||
private readonly RapidGossipSync _rapidGossipSync;
|
||||
private readonly NetworkGraph _networkGraph;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly ILogger<LDKRapidGossipSyncer> _logger;
|
||||
private CancellationTokenSource? _cts;
|
||||
private TaskCompletionSource _configUpdated = new();
|
||||
|
||||
public LDKRapidGossipSyncer(LDKNode ldkNode,
|
||||
RapidGossipSync rapidGossipSync,
|
||||
NetworkGraph networkGraph,
|
||||
IHttpClientFactory httpClientFactory, ILogger<LDKRapidGossipSyncer> logger)
|
||||
{
|
||||
_ldkNode = ldkNode;
|
||||
_rapidGossipSync = rapidGossipSync;
|
||||
_networkGraph = networkGraph;
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
ldkNode.ConfigUpdated += OnConfigUpdated;
|
||||
_ldkNode.ConfigUpdated += OnConfigUpdated;
|
||||
_ = UpdateNetworkGraph();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
@ -38,7 +49,7 @@ public class LDKRapidGossipSyncer(
|
||||
try
|
||||
{
|
||||
_configUpdated = new();
|
||||
var config = await ldkNode.GetConfig();
|
||||
var config = await _ldkNode.GetConfig();
|
||||
if (config.RapidGossipSyncUrl is null)
|
||||
{
|
||||
try
|
||||
@ -53,30 +64,30 @@ public class LDKRapidGossipSyncer(
|
||||
// wait until config is updated or _cts is cancelled
|
||||
}
|
||||
|
||||
var timestamp = networkGraph.get_last_rapid_gossip_sync_timestamp() is Option_u32Z.Option_u32Z_Some some
|
||||
var timestamp = _networkGraph.get_last_rapid_gossip_sync_timestamp() is Option_u32Z.Option_u32Z_Some some
|
||||
? some.some : 0;
|
||||
var uri = new Uri(config.RapidGossipSyncUrl, $"/snapshot/{timestamp}");
|
||||
var response = await httpClientFactory.CreateClient("rgs").GetAsync(uri, _cts.Token);
|
||||
var response = await _httpClientFactory.CreateClient("rgs").GetAsync(uri, _cts.Token);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
logger.LogError("Failed to download snapshot from {uri}", uri);
|
||||
_logger.LogError("Failed to download snapshot from {uri}", uri);
|
||||
continue;
|
||||
}
|
||||
|
||||
var snapshot = await response.Content.ReadAsByteArrayAsync();
|
||||
var result =
|
||||
rapidGossipSync.update_network_graph_no_std(snapshot,
|
||||
_rapidGossipSync.update_network_graph_no_std(snapshot,
|
||||
Option_u64Z.some(DateTime.Now.ToUnixTimestamp()));
|
||||
if (result is Result_u32GraphSyncErrorZ.Result_u32GraphSyncErrorZ_Err err)
|
||||
{
|
||||
switch (err.err)
|
||||
{
|
||||
case GraphSyncError.GraphSyncError_DecodeError graphSyncErrorDecodeError:
|
||||
logger.LogError(
|
||||
_logger.LogError(
|
||||
$"Failed to decode snapshot from {uri} with error {graphSyncErrorDecodeError.decode_error.GetType().Name}");
|
||||
break;
|
||||
case GraphSyncError.GraphSyncError_LightningError graphSyncErrorLightningError:
|
||||
logger.LogError(
|
||||
_logger.LogError(
|
||||
$"Failed to update network graph with error {graphSyncErrorLightningError.lightning_error.get_err()}");
|
||||
// config = await _ldkNode.GetConfig();
|
||||
// await _ldkNode.UpdateConfig(config);
|
||||
@ -95,7 +106,7 @@ public class LDKRapidGossipSyncer(
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogError(e, "Error while updating network graph");
|
||||
_logger.LogError(e, "Error while updating network graph");
|
||||
await Task.Delay(10000, _cts.Token);
|
||||
}
|
||||
}
|
||||
@ -103,7 +114,7 @@ public class LDKRapidGossipSyncer(
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
ldkNode.ConfigUpdated -= OnConfigUpdated;
|
||||
_ldkNode.ConfigUpdated -= OnConfigUpdated;
|
||||
if (_cts is not null)
|
||||
{
|
||||
await _cts.CancelAsync();
|
||||
|
||||
@ -5,9 +5,16 @@ using UInt128 = org.ldk.util.UInt128;
|
||||
|
||||
namespace BTCPayApp.Core.LDK;
|
||||
|
||||
public class LDKSignerProvider(KeysManager innerSigner, LDKNode ldkNode) : SignerProviderInterface
|
||||
public class LDKSignerProvider : SignerProviderInterface
|
||||
{
|
||||
private readonly SignerProvider _innerSigner = innerSigner.as_SignerProvider();
|
||||
private readonly LDKNode _ldkNode;
|
||||
private readonly SignerProvider _innerSigner;
|
||||
|
||||
public LDKSignerProvider(KeysManager innerSigner, LDKNode ldkNode)
|
||||
{
|
||||
_ldkNode = ldkNode;
|
||||
_innerSigner = innerSigner.as_SignerProvider();
|
||||
}
|
||||
|
||||
public byte[] generate_channel_keys_id(bool inbound, long channel_value_satoshis, UInt128 user_channel_id)
|
||||
{
|
||||
@ -26,13 +33,13 @@ public class LDKSignerProvider(KeysManager innerSigner, LDKNode ldkNode) : Signe
|
||||
|
||||
public Result_CVec_u8ZNoneZ get_destination_script(byte[] channel_keys_id)
|
||||
{
|
||||
var script = ldkNode.DeriveScript().GetAwaiter().GetResult();
|
||||
var script = _ldkNode.DeriveScript().GetAwaiter().GetResult();
|
||||
return Result_CVec_u8ZNoneZ.ok(script.ToBytes());
|
||||
}
|
||||
|
||||
public Result_ShutdownScriptNoneZ get_shutdown_scriptpubkey()
|
||||
{
|
||||
var script = ldkNode.DeriveScript().GetAwaiter().GetResult();
|
||||
var script = _ldkNode.DeriveScript().GetAwaiter().GetResult();
|
||||
|
||||
if (!script.IsScriptType(ScriptType.Witness))
|
||||
throw new NotSupportedException("Generated a non witness script.");
|
||||
|
||||
@ -149,6 +149,19 @@ public class LDKTcpDescriptor : SocketDescriptorInterface
|
||||
}
|
||||
}
|
||||
|
||||
private void Resume()
|
||||
{
|
||||
try
|
||||
{
|
||||
_readSemaphore.Release();
|
||||
_logger.LogDebug("Resuming read");
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
public long send_data(byte[] data, bool resumeRead)
|
||||
{
|
||||
try
|
||||
@ -156,6 +169,7 @@ public class LDKTcpDescriptor : SocketDescriptorInterface
|
||||
_logger.LogTrace("Sending {Bytes} bytes of data to peer", data.Length);
|
||||
var result = _tcpClient.Client.Send(data);
|
||||
_logger.LogTrace("Sent {Bytes} bytes of data to peer", result);
|
||||
if (resumeRead) Resume();
|
||||
return result;
|
||||
}
|
||||
catch (Exception e)
|
||||
|
||||
@ -1,3 +1,8 @@
|
||||
namespace BTCPayApp.Core.LDK;
|
||||
|
||||
public class LDKWalletLogger(LDKWalletLoggerFactory ldkWalletLoggerFactory) : LDKLogger(ldkWalletLoggerFactory);
|
||||
public class LDKWalletLogger : LDKLogger
|
||||
{
|
||||
public LDKWalletLogger(LDKWalletLoggerFactory ldkWalletLoggerFactory) : base(ldkWalletLoggerFactory)
|
||||
{
|
||||
}
|
||||
}
|
||||
@ -2,8 +2,15 @@
|
||||
|
||||
namespace BTCPayApp.Core.LDK;
|
||||
|
||||
public class LDKWalletLoggerFactory(ILoggerFactory loggerFactory) : ILoggerFactory
|
||||
public class LDKWalletLoggerFactory : ILoggerFactory
|
||||
{
|
||||
private readonly ILoggerFactory _inner;
|
||||
|
||||
public LDKWalletLoggerFactory(ILoggerFactory loggerFactory)
|
||||
{
|
||||
_inner = loggerFactory;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
//ignore as this is scoped
|
||||
@ -11,7 +18,7 @@ public class LDKWalletLoggerFactory(ILoggerFactory loggerFactory) : ILoggerFacto
|
||||
|
||||
public void AddProvider(ILoggerProvider provider)
|
||||
{
|
||||
loggerFactory.AddProvider(provider);
|
||||
_inner.AddProvider(provider);
|
||||
}
|
||||
|
||||
public List<string> Logs { get; } = new List<string>();
|
||||
@ -19,11 +26,11 @@ public class LDKWalletLoggerFactory(ILoggerFactory loggerFactory) : ILoggerFacto
|
||||
public ILogger CreateLogger(string category)
|
||||
{
|
||||
var categoryName = string.IsNullOrWhiteSpace(category) ? "LDK" : $"LDK.{category}";
|
||||
LoggerWrapper logger = new LoggerWrapper(loggerFactory.CreateLogger(categoryName));
|
||||
LoggerWrapper logger = new LoggerWrapper(_inner.CreateLogger(categoryName));
|
||||
|
||||
logger.LogEvent += (sender, message) =>
|
||||
Logs.Add(DateTime.Now.ToShortTimeString() + " " + categoryName + message);
|
||||
|
||||
return logger;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2,51 +2,42 @@
|
||||
|
||||
namespace BTCPayApp.Core.LDK;
|
||||
|
||||
public class LightningAPIKeyManager(ConfigProvider configProvider)
|
||||
public class LightningAPIKeyManager
|
||||
{
|
||||
private const string LightningAPIKeyConfigKey = "LightningAPIKeys";
|
||||
private readonly ConfigProvider _configProvider;
|
||||
|
||||
public async Task<APIKey> GetKeyForStore(string storeId, APIKeyPermission permission)
|
||||
|
||||
public LightningAPIKeyManager(ConfigProvider configProvider)
|
||||
{
|
||||
return await GetOrCreate($"BTCPay Store {storeId}", permission);
|
||||
_configProvider = configProvider;
|
||||
}
|
||||
|
||||
public async Task<List<APIKey>> List()
|
||||
{
|
||||
var keys = await _configProvider.Get<List<APIKey>>(LightningAPIKeyConfigKey) ?? [];
|
||||
return keys;
|
||||
}
|
||||
|
||||
public async Task Revoke(string key)
|
||||
{
|
||||
var keys = await List();
|
||||
if (keys.RemoveAll(k => k.Key == key)>0)
|
||||
await configProvider.Set(LightningAPIKeyConfigKey, keys, true);
|
||||
if(keys.RemoveAll(k => k.Key == key)>0)
|
||||
await _configProvider.Set(LightningAPIKeyConfigKey, keys, true);
|
||||
}
|
||||
|
||||
public async Task<APIKey> Create(string name, APIKeyPermission permission)
|
||||
{
|
||||
var keys = await List();
|
||||
var newKey = new APIKey(Guid.NewGuid().ToString(), name, permission);
|
||||
keys.Add(newKey);
|
||||
await _configProvider.Set(LightningAPIKeyConfigKey, keys, true);
|
||||
return newKey;
|
||||
|
||||
}
|
||||
public async Task<bool> CheckPermission(string key, APIKeyPermission permission)
|
||||
{
|
||||
var keys = await List();
|
||||
return keys.Any(k => k.Key == key && k.Permission >= permission);
|
||||
}
|
||||
|
||||
private async Task<List<APIKey>> List()
|
||||
{
|
||||
var keys = await configProvider.Get<List<APIKey>>(LightningAPIKeyConfigKey) ?? [];
|
||||
return keys;
|
||||
}
|
||||
|
||||
private async Task<APIKey?> Get(string name, APIKeyPermission permission)
|
||||
{
|
||||
var keys = await List();
|
||||
return keys.FirstOrDefault(k => k.Name == name && k.Permission == permission);
|
||||
}
|
||||
|
||||
private async Task<APIKey> Create(string name, APIKeyPermission permission)
|
||||
{
|
||||
var keys = await List();
|
||||
var newKey = new APIKey(Guid.NewGuid().ToString(), name, permission);
|
||||
keys.Add(newKey);
|
||||
await configProvider.Set(LightningAPIKeyConfigKey, keys, true);
|
||||
return newKey;
|
||||
}
|
||||
|
||||
private async Task<APIKey> GetOrCreate(string name, APIKeyPermission permission)
|
||||
{
|
||||
return await Get(name, permission) ?? await Create(name, permission);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2,24 +2,31 @@
|
||||
|
||||
namespace BTCPayApp.Core.LDK;
|
||||
|
||||
public class LoggerWrapper(ILogger inner) : ILogger
|
||||
public class LoggerWrapper : ILogger
|
||||
{
|
||||
private readonly ILogger _inner;
|
||||
|
||||
public LoggerWrapper(ILogger inner)
|
||||
{
|
||||
_inner = inner;
|
||||
}
|
||||
|
||||
public IDisposable? BeginScope<TState>(TState state) where TState : notnull
|
||||
{
|
||||
return inner.BeginScope(state);
|
||||
return _inner.BeginScope(state);
|
||||
}
|
||||
|
||||
public bool IsEnabled(LogLevel logLevel)
|
||||
{
|
||||
return inner.IsEnabled(logLevel);
|
||||
return _inner.IsEnabled(logLevel);
|
||||
}
|
||||
|
||||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception,
|
||||
Func<TState, Exception?, string> formatter)
|
||||
{
|
||||
inner.Log(logLevel, eventId, state, exception, formatter);
|
||||
_inner.Log(logLevel, eventId, state, exception, formatter);
|
||||
LogEvent?.Invoke(this, formatter(state, exception));
|
||||
}
|
||||
|
||||
public event EventHandler<string>? LogEvent;
|
||||
}
|
||||
}
|
||||
@ -75,8 +75,10 @@ public class PaymentsManager :
|
||||
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 now = DateTimeOffset.UtcNow;
|
||||
var epoch = now.ToUnixTimeSeconds();
|
||||
|
||||
var descHashBytes = Sha256.from_bytes(descriptionHash.ToBytes());
|
||||
var lsp = await _ldkNode.GetJITLSPService();
|
||||
|
||||
@ -115,7 +117,7 @@ public class PaymentsManager :
|
||||
};
|
||||
|
||||
var parsedOriginalInvoice = BOLT11PaymentRequest.Parse(originalInvoice.to_str(), _network);
|
||||
var lp = new AppLightningPayment
|
||||
var lp = new AppLightningPayment()
|
||||
{
|
||||
Inbound = true,
|
||||
PaymentId = "default",
|
||||
|
||||
13
BTCPayApp.Core/LDK/PeersChangedEventArgs.cs
Normal file
13
BTCPayApp.Core/LDK/PeersChangedEventArgs.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using BTCPayServer.Lightning;
|
||||
|
||||
namespace BTCPayApp.Core.LDK;
|
||||
|
||||
public class PeersChangedEventArgs : EventArgs
|
||||
{
|
||||
public List<NodeInfo> PeerNodeIds { get; set; }
|
||||
|
||||
public PeersChangedEventArgs(List<NodeInfo> peerNodeIds)
|
||||
{
|
||||
PeerNodeIds = peerNodeIds;
|
||||
}
|
||||
}
|
||||
@ -1,17 +0,0 @@
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.JsonConverters;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayApp.Core.Models;
|
||||
|
||||
public class CreatePosInvoiceRequest
|
||||
{
|
||||
public string? AppId { get; set; }
|
||||
public List<AppCartItem>? Cart { get; set; }
|
||||
public int? DiscountPercent { get; set; }
|
||||
[JsonConverter(typeof(NumericStringJsonConverter))]
|
||||
public decimal? DiscountAmount { get; set; }
|
||||
[JsonConverter(typeof(NumericStringJsonConverter))]
|
||||
public decimal? Tip { get; set; }
|
||||
public string? PosData { get; set; }
|
||||
}
|
||||
@ -2,10 +2,7 @@ namespace BTCPayApp.Core.Models;
|
||||
|
||||
public class CreateStoreData
|
||||
{
|
||||
public string? Name { get; set; }
|
||||
public string DefaultCurrency { get; set; } = null!;
|
||||
public string? DefaultCurrency { get; set; }
|
||||
public string? RecommendedExchangeId { get; set; }
|
||||
public Dictionary<string, string>? Exchanges { get; set; }
|
||||
public string? PreferredExchangeId { get; set; }
|
||||
public bool CanEditPreferredExchange { get; set; }
|
||||
public bool CanAutoCreate { get; set; }
|
||||
}
|
||||
|
||||
@ -1,10 +0,0 @@
|
||||
namespace BTCPayApp.Core.Models;
|
||||
|
||||
public class LoginInfoResult
|
||||
{
|
||||
public string? Email { get; set; }
|
||||
public bool? HasPassword { get; set; }
|
||||
public bool? IsEmailConfirmed { get; set; }
|
||||
public bool? IsApproved { get; set; }
|
||||
public bool? IsDisabled { get; set; }
|
||||
}
|
||||
@ -1,28 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayApp.Core.Models
|
||||
{
|
||||
public class NfcLnUrlRecord
|
||||
{
|
||||
public byte[]? Payload { get; set; }
|
||||
|
||||
//
|
||||
// Summary:
|
||||
// LnUrl
|
||||
public string? LnUrl { get; set; }
|
||||
|
||||
//
|
||||
// Summary:
|
||||
// String formatted payload
|
||||
public string? Message { get; set; }
|
||||
|
||||
//
|
||||
// Summary:
|
||||
// Two letters ISO 639-1 Language Code (ex: en, fr, de...)
|
||||
public string? LanguageCode { get; set; }
|
||||
}
|
||||
}
|
||||
@ -1,15 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayApp.Core.Models
|
||||
{
|
||||
public class SubmitLnUrlRequest
|
||||
{
|
||||
public string? Lnurl { get; set; }
|
||||
public string? InvoiceId { get; set; }
|
||||
public long? Amount { get; set; }
|
||||
}
|
||||
}
|
||||
@ -1,35 +1,38 @@
|
||||
using System.Text.Json;
|
||||
using BTCPayApp.Core.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using BTCPayApp.Core.Contracts;
|
||||
using Serilog.Core;
|
||||
using Serilog.Events;
|
||||
using Serilog.Parsing;
|
||||
|
||||
namespace BTCPayApp.Core.Services;
|
||||
|
||||
public class LoggingService(IDbContextFactory<LogDbContext> dbContextFactory)
|
||||
public class LoggingService(IDataDirectoryProvider dataDirectory, LoggingLevelSwitch levelSwitch)
|
||||
{
|
||||
public static readonly LogEventLevel[] Levels = [LogEventLevel.Verbose, LogEventLevel.Debug, LogEventLevel.Information, LogEventLevel.Warning, LogEventLevel.Error];
|
||||
|
||||
public async Task<string?> GetLatestLogAsync(LogEventLevel minLevel, int count = 100)
|
||||
public async Task<string> GetLatestLogAsync()
|
||||
{
|
||||
await using var dbContext = await dbContextFactory.CreateDbContextAsync();
|
||||
var levels = Levels.Skip((int)minLevel).Select(l => l.ToString());
|
||||
var parser = new MessageTemplateParser();
|
||||
var logs = await dbContext.Logs
|
||||
.Where(e => levels.Contains(e.Level))
|
||||
.OrderByDescending(e => e.TimeStamp)
|
||||
.Take(count)
|
||||
.ToArrayAsync();
|
||||
var lines = logs.Select(l =>
|
||||
try
|
||||
{
|
||||
var level = Enum.Parse<LogEventLevel>(l.Level, true);
|
||||
var tmpl = parser.Parse(l.RenderedMessage);
|
||||
var props = JsonSerializer.Deserialize<Dictionary<string, object>>(l.Properties);
|
||||
var properties = props?.Select(p => new LogEventProperty(p.Key, new ScalarValue(p.Value))) ?? [];
|
||||
var e = new LogEvent(l.TimeStamp, level, null, tmpl, properties);
|
||||
var end = string.IsNullOrEmpty(l.Exception) ? string.Empty : $"{Environment.NewLine}{l.Exception}";
|
||||
return $"[{e.Timestamp:yyyy-MM-dd HH:mm:ss} {e.Level.ToString()[..3].ToUpperInvariant()}] {e.RenderMessage()} ({e.Properties["SourceContext"]}){end}{Environment.NewLine}";
|
||||
});
|
||||
return logs.Length != 0 ? string.Join("", lines.Reverse()) : null;
|
||||
var logDir = Path.Combine(await dataDirectory.GetAppDataDirectory(), "logs");
|
||||
var latestLog = Directory.GetFiles(logDir)
|
||||
.OrderByDescending(f => f)
|
||||
.FirstOrDefault();
|
||||
|
||||
return latestLog != null
|
||||
? await File.ReadAllTextAsync(latestLog)
|
||||
: "No logs available";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return $"Error reading logs: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string> GetAppLogFilePath()
|
||||
{
|
||||
return Path.Combine(await dataDirectory.GetAppDataDirectory(), "logs");
|
||||
}
|
||||
|
||||
public LogEventLevel LogLevel
|
||||
{
|
||||
get => levelSwitch.MinimumLevel;
|
||||
set => levelSwitch.MinimumLevel = value;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
using System.Linq.Expressions;
|
||||
using BTCPayApp.Core.BTCPayServer;
|
||||
using BTCPayApp.Core.Data;
|
||||
using BTCPayApp.Core.Helpers;
|
||||
@ -9,37 +8,26 @@ using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace BTCPayApp.Core.Wallet;
|
||||
|
||||
public class LightningNodeManager(
|
||||
IDbContextFactory<AppDbContext> dbContextFactory,
|
||||
ILogger<LightningNodeManager> logger,
|
||||
OnChainWalletManager onChainWalletManager,
|
||||
BTCPayConnectionManager btcPayConnectionManager,
|
||||
IServiceScopeFactory serviceScopeFactory)
|
||||
: BaseHostedService(logger)
|
||||
public class LightningNodeManager : BaseHostedService
|
||||
{
|
||||
public const string PaymentMethodId = "BTC-LN";
|
||||
|
||||
private readonly IDbContextFactory<AppDbContext> _dbContextFactory;
|
||||
private readonly ILogger<LightningNodeManager> _logger;
|
||||
private readonly OnChainWalletManager _onChainWalletManager;
|
||||
private readonly BTCPayConnectionManager _btcPayConnectionManager;
|
||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||
|
||||
private IServiceScope? _nodeScope;
|
||||
public LDKNode? Node => _nodeScope?.ServiceProvider.GetService<LDKNode>();
|
||||
private LightningNodeState _state = LightningNodeState.Init;
|
||||
public bool IsHubConnected => _btcPayConnectionManager.ConnectionState is BTCPayConnectionState.ConnectedAsPrimary;
|
||||
private async Task<bool> IsOnchainLightningDerivationConfigured() => (await _onChainWalletManager.GetConfig())?.Derivations.ContainsKey(WalletDerivation.LightningScripts) is true;
|
||||
public async Task<bool> CanConfigureLightningNode () => IsHubConnected && await _onChainWalletManager.IsConfigured() && !await IsOnchainLightningDerivationConfigured() && State == LightningNodeState.NotConfigured;
|
||||
// public string? ConnectionString => IsOnchainLightningDerivationConfigured && _accountManager.GetUserInfo() is {} acc
|
||||
// ? $"type=app;user={acc.UserId}": null;
|
||||
|
||||
public bool IsActive => State == LightningNodeState.Loaded;
|
||||
public bool IsHubConnected => btcPayConnectionManager.ConnectionState is BTCPayConnectionState.ConnectedAsPrimary;
|
||||
private async Task<bool> IsOnchainLightningDerivationConfigured () => (await onChainWalletManager.GetConfig())?.Derivations.ContainsKey(WalletDerivation.LightningScripts) is true;
|
||||
public async Task<bool> CanConfigureLightningNode () => IsHubConnected && await onChainWalletManager.IsConfigured() && !await IsOnchainLightningDerivationConfigured() && State == LightningNodeState.NotConfigured;
|
||||
public event AsyncEventHandler<(LightningNodeState Old, LightningNodeState New)>? StateChanged;
|
||||
public LDKNode? Node
|
||||
{
|
||||
get
|
||||
{
|
||||
try
|
||||
{
|
||||
return _nodeScope?.ServiceProvider.GetService<LDKNode>();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogError(e, "Error while getting LDKNode");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public LightningNodeState State
|
||||
{
|
||||
@ -50,29 +38,47 @@ public class LightningNodeManager(
|
||||
return;
|
||||
var old = _state;
|
||||
_state = value;
|
||||
logger.LogInformation("Lightning node state changed: {Old} -> {State}", old, _state);
|
||||
_logger.LogInformation("Lightning node state changed: {Old} -> {State}", old, _state);
|
||||
StateChanged?.Invoke(this, (old, value));
|
||||
}
|
||||
}
|
||||
|
||||
public event AsyncEventHandler<(LightningNodeState Old, LightningNodeState New)>? StateChanged;
|
||||
|
||||
public LightningNodeManager(
|
||||
IDbContextFactory<AppDbContext> dbContextFactory,
|
||||
ILogger<LightningNodeManager> logger,
|
||||
OnChainWalletManager onChainWalletManager,
|
||||
BTCPayConnectionManager btcPayConnectionManager,
|
||||
IServiceScopeFactory serviceScopeFactory) : base(logger)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_logger = logger;
|
||||
_onChainWalletManager = onChainWalletManager;
|
||||
_btcPayConnectionManager = btcPayConnectionManager;
|
||||
_serviceScopeFactory = serviceScopeFactory;
|
||||
}
|
||||
|
||||
public async Task StartNode()
|
||||
{
|
||||
if (_nodeScope is not null || State is LightningNodeState.Loaded or LightningNodeState.NotConfigured)
|
||||
return;
|
||||
|
||||
if (onChainWalletManager.State is not OnChainWalletState.Loaded)
|
||||
{
|
||||
State = LightningNodeState.WaitingForConnection;
|
||||
return;
|
||||
}
|
||||
|
||||
logger.LogInformation("Starting lightning node");
|
||||
_logger.LogInformation("Starting lightning node");
|
||||
await ControlSemaphore.WaitAsync();
|
||||
try
|
||||
{
|
||||
if (_onChainWalletManager.State is not OnChainWalletState.Loaded)
|
||||
{
|
||||
State = LightningNodeState.WaitingForConnection;
|
||||
|
||||
return;
|
||||
}
|
||||
if (_nodeScope is null)
|
||||
{
|
||||
_nodeScope = serviceScopeFactory.CreateScope();
|
||||
_nodeScope = _serviceScopeFactory.CreateScope();
|
||||
CancellationTokenSource = new CancellationTokenSource();
|
||||
}
|
||||
await Node!.StartAsync(CancellationTokenSource.Token);
|
||||
@ -82,7 +88,7 @@ public class LightningNodeManager(
|
||||
catch (Exception e)
|
||||
{
|
||||
_nodeScope?.Dispose();
|
||||
logger.LogError(e, "Error while starting lightning node");
|
||||
_logger.LogError(e, "Error while starting lightning node");
|
||||
_nodeScope = null;
|
||||
State = LightningNodeState.Error;
|
||||
}
|
||||
@ -101,12 +107,12 @@ public class LightningNodeManager(
|
||||
await ControlSemaphore.WaitAsync();
|
||||
try
|
||||
{
|
||||
logger.LogInformation("Stopping lightning node");
|
||||
_logger.LogInformation("Stopping lightning node");
|
||||
if (Node != null) await Node.StopAsync(CancellationToken.None);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogError(e, "Error while stopping lightning node");
|
||||
_logger.LogError(e, "Error while stopping lightning node");
|
||||
}
|
||||
finally
|
||||
{
|
||||
@ -127,8 +133,8 @@ public class LightningNodeManager(
|
||||
await ControlSemaphore.WaitAsync();
|
||||
try
|
||||
{
|
||||
await onChainWalletManager.RemoveDerivation(WalletDerivation.LightningScripts);
|
||||
await using var context = await dbContextFactory.CreateDbContextAsync();
|
||||
await _onChainWalletManager.RemoveDerivation(WalletDerivation.LightningScripts);
|
||||
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||
context.LightningPayments.RemoveRange(context.LightningPayments);
|
||||
// context.OutboxItems.RemoveRange(context.OutboxItems);
|
||||
context.Settings.RemoveRange(context.Settings.Where(s => s.Key.StartsWith("ln:")));
|
||||
@ -150,13 +156,13 @@ public class LightningNodeManager(
|
||||
if (!IsHubConnected)
|
||||
throw new InvalidOperationException("Cannot configure lightning node without BTCPay connection");
|
||||
|
||||
if (!await onChainWalletManager.IsConfigured())
|
||||
if (!await _onChainWalletManager.IsConfigured())
|
||||
throw new InvalidOperationException("Cannot configure lightning node without on-chain wallet configuration");
|
||||
if (await IsOnchainLightningDerivationConfigured())
|
||||
throw new InvalidOperationException("On-chain wallet is already configured with a lightning derivation");
|
||||
|
||||
logger.LogInformation("Generating lightning node");
|
||||
await onChainWalletManager.AddDerivation(WalletDerivation.LightningScripts, "Lightning", null);
|
||||
_logger.LogInformation("Generating lightning node");
|
||||
await _onChainWalletManager.AddDerivation(WalletDerivation.LightningScripts, "Lightning", null);
|
||||
// await _onChainWalletManager.AddDerivation(WalletDerivation.SpendableOutputs, "Lightning Spendables", null);
|
||||
State = LightningNodeState.WaitingForConnection;
|
||||
}
|
||||
@ -213,7 +219,7 @@ public class LightningNodeManager(
|
||||
newState = LightningNodeState.WaitingForConnection;
|
||||
break;
|
||||
}
|
||||
if (!await onChainWalletManager.IsConfigured() || !await IsOnchainLightningDerivationConfigured())
|
||||
if (!await _onChainWalletManager.IsConfigured() || !await IsOnchainLightningDerivationConfigured())
|
||||
{
|
||||
newState = LightningNodeState.NotConfigured;
|
||||
break;
|
||||
@ -238,13 +244,13 @@ public class LightningNodeManager(
|
||||
StateChanged += OnStateChanged;
|
||||
await StateChanged.Invoke(this, (LightningNodeState.Init, LightningNodeState.Init));
|
||||
// _btcPayConnectionManager.ConnectionChanged += OnConnectionChanged;
|
||||
onChainWalletManager.StateChanged += OnChainWalletManagerOnStateChanged;
|
||||
_onChainWalletManager.StateChanged += OnChainWalletManagerOnStateChanged;
|
||||
}
|
||||
|
||||
protected override Task ExecuteStopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
// _btcPayConnectionManager.ConnectionChanged -= OnConnectionChanged;
|
||||
onChainWalletManager.StateChanged += OnChainWalletManagerOnStateChanged;
|
||||
_onChainWalletManager.StateChanged += OnChainWalletManagerOnStateChanged;
|
||||
StateChanged -= OnStateChanged;
|
||||
_nodeScope?.Dispose();
|
||||
return Task.CompletedTask;
|
||||
|
||||
@ -1,8 +1,5 @@
|
||||
using System.Text.Json.Serialization;
|
||||
namespace BTCPayApp.Core.Wallet;
|
||||
|
||||
namespace BTCPayApp.Core.Wallet;
|
||||
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public enum LightningNodeState
|
||||
{
|
||||
Init,
|
||||
@ -11,5 +8,6 @@ public enum LightningNodeState
|
||||
Loading,
|
||||
Loaded,
|
||||
Stopped,
|
||||
Error
|
||||
Error,
|
||||
Inactive
|
||||
}
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using BTCPayApp.Core.Backup;
|
||||
using BTCPayApp.Core.BTCPayServer;
|
||||
using BTCPayApp.Core.Contracts;
|
||||
@ -730,7 +729,6 @@ public class OnChainWalletManager : BaseHostedService
|
||||
}
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public enum OnChainWalletState
|
||||
{
|
||||
Init,
|
||||
|
||||
@ -12,7 +12,6 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="8.0.11" />
|
||||
<PackageReference Include="Microsoft.JSInterop" Version="8.0.16" />
|
||||
<PackageReference Include="Plugin.Fingerprint" Version="3.0.0-beta.1" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@ -1,12 +0,0 @@
|
||||
using BTCPayApp.Core.Contracts;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace BTCPayApp.Desktop.Services;
|
||||
|
||||
public class EmailService(IJSRuntime jSRuntime) : IEmailService
|
||||
{
|
||||
public async Task SendAsync(string subject, string body, string recipient, string? attachFilePath = null)
|
||||
{
|
||||
await jSRuntime.InvokeVoidAsync("Interop.sendEmail", subject, body, recipient);
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
using BTCPayApp.Core.Contracts;
|
||||
using BTCPayApp.Desktop.Services;
|
||||
using BTCPayApp.Core.Data;
|
||||
using BTCPayApp.Core.Helpers;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Plugin.Fingerprint.Abstractions;
|
||||
|
||||
@ -14,9 +15,9 @@ public static class StartupExtensions
|
||||
options.ApplicationDiscriminator = "BTCPayApp";
|
||||
});
|
||||
serviceCollection.AddSingleton<IDataDirectoryProvider, DesktopDataDirectoryProvider>();
|
||||
// serviceCollection.AddSingleton<IConfigProvider, DesktopConfigProvider>();
|
||||
serviceCollection.AddSingleton<ISecureConfigProvider, DesktopSecureConfigProvider>();
|
||||
serviceCollection.AddSingleton<IFingerprint, StubFingerprintProvider>();
|
||||
serviceCollection.AddScoped<IEmailService, EmailService>();
|
||||
|
||||
return serviceCollection;
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Razor">
|
||||
<Project Sdk="Microsoft.NET.Sdk.Razor">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net8.0-android;</TargetFrameworks>
|
||||
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('linux'))">net8.0-android;</TargetFrameworks>
|
||||
@ -23,7 +23,7 @@
|
||||
<ApplicationVersion>1</ApplicationVersion>
|
||||
|
||||
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">14.2</SupportedOSPlatformVersion>
|
||||
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">26.0</SupportedOSPlatformVersion>
|
||||
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">24.0</SupportedOSPlatformVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">
|
||||
@ -41,7 +41,12 @@
|
||||
<EmbedAssembliesIntoApk>True</EmbedAssembliesIntoApk>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android' And $(Configuration) == 'Debug'">
|
||||
<DebugSymbols>True</DebugSymbols>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android' And '$(Configuration)' == 'Release'">
|
||||
<DebugSymbols>False</DebugSymbols>
|
||||
<PublishTrimmed>False</PublishTrimmed>
|
||||
<AndroidKeyStore>True</AndroidKeyStore>
|
||||
<AndroidSigningKeyStore>../btcpayapp.keystore</AndroidSigningKeyStore>
|
||||
@ -70,7 +75,6 @@
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.3" />
|
||||
<PackageReference Include="Microsoft.Maui.Controls" Version="8.0.100" />
|
||||
<PackageReference Include="Microsoft.Maui.Essentials" Version="8.0.100" />
|
||||
<PackageReference Include="Plugin.NFC" Version="0.1.26" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">
|
||||
|
||||
@ -1,23 +1,16 @@
|
||||
using BTCPayApp.Core.Extensions;
|
||||
using BTCPayApp.Core.Contracts;
|
||||
using BTCPayApp.Core.Extensions;
|
||||
using BTCPayApp.Maui.Services;
|
||||
using BTCPayApp.UI;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Maui.LifecycleEvents;
|
||||
using Plugin.Fingerprint;
|
||||
using BTCPayApp.Core.Contracts;
|
||||
|
||||
|
||||
|
||||
|
||||
#if ANDROID
|
||||
using Android.Content;
|
||||
#endif
|
||||
using Serilog;
|
||||
|
||||
namespace BTCPayApp.Maui;
|
||||
|
||||
public static class MauiProgram
|
||||
{
|
||||
public static IServiceProvider Services { get; private set; }
|
||||
|
||||
public static MauiApp CreateMauiApp()
|
||||
{
|
||||
var builder = MauiApp.CreateBuilder();
|
||||
@ -32,50 +25,28 @@ public static class MauiProgram
|
||||
builder.Services.AddBTCPayAppUIServices();
|
||||
builder.Services.ConfigureBTCPayAppMaui();
|
||||
builder.Services.ConfigureBTCPayAppCore();
|
||||
builder.Services.AddSingleton<INfcService, NfcService>();
|
||||
|
||||
var serviceProvider = builder.Services.BuildServiceProvider();
|
||||
var logger = serviceProvider.GetRequiredService<ILogger<MauiApp>>();
|
||||
builder.Services.AddSingleton<IEmailService, EmailService>();
|
||||
|
||||
builder.ConfigureLifecycleEvents(events =>
|
||||
{
|
||||
// https://learn.microsoft.com/de-de/dotnet/maui/fundamentals/app-lifecycle#platform-lifecycle-events
|
||||
#if ANDROID
|
||||
events.AddAndroid(android => android
|
||||
.OnCreate((activity, _) =>
|
||||
.OnStart((activity) => LogEvent(nameof(AndroidLifecycle.OnStart)))
|
||||
.OnCreate((activity, bundle) =>
|
||||
{
|
||||
logger.LogDebug("Lifecycle event: {Name}", nameof(AndroidLifecycle.OnCreate));
|
||||
CrossFingerprint.SetCurrentActivityResolver(() => activity);
|
||||
})
|
||||
.OnStart(_ =>
|
||||
{
|
||||
logger.LogDebug("Lifecycle event: {Name}", nameof(AndroidLifecycle.OnStart));
|
||||
|
||||
var context = Android.App.Application.Context;
|
||||
var intent = new Intent(context, typeof(HubConnectionForegroundService));
|
||||
context.StopService(intent); // App is in foreground — stop background service
|
||||
LogEvent(nameof(AndroidLifecycle.OnCreate));
|
||||
})
|
||||
.OnStop(_ =>
|
||||
.OnStop((activity) => { LogEvent(nameof(AndroidLifecycle.OnStop)); }).OnDestroy(activity =>
|
||||
{
|
||||
logger.LogDebug("Lifecycle event: {Name}", nameof(AndroidLifecycle.OnStop));
|
||||
|
||||
var context = Android.App.Application.Context;
|
||||
var intent = new Intent(context, typeof(HubConnectionForegroundService));
|
||||
//context.StartForegroundService(intent); // App is in background — start service
|
||||
})
|
||||
.OnDestroy(activity =>
|
||||
{
|
||||
logger.LogDebug("Lifecycle event: {Name}", nameof(AndroidLifecycle.OnDestroy));
|
||||
// This event is called whenever we use File.OpenReadStream().CopyToAsync(fs)
|
||||
// Immersive is being used to prevent the services from being disposed when the above method is used.
|
||||
if (activity.Immersive)
|
||||
{
|
||||
IPlatformApplication.Current?.Services.GetRequiredService<HostedServiceInitializer>().Dispose();
|
||||
}
|
||||
// Stop foreground service
|
||||
var context = Android.App.Application.Context;
|
||||
var intent = new Intent(context, typeof(HubConnectionForegroundService));
|
||||
context.StopService(intent);
|
||||
}));
|
||||
#endif
|
||||
#if IOS
|
||||
@ -83,20 +54,26 @@ public static class MauiProgram
|
||||
.OnActivated((app) => LogEvent(nameof(iOSLifecycle.OnActivated)))
|
||||
.OnResignActivation((app) => LogEvent(nameof(iOSLifecycle.OnResignActivation)))
|
||||
.DidEnterBackground((app) => LogEvent(nameof(iOSLifecycle.DidEnterBackground)))
|
||||
.WillTerminate((app) => LogEvent(nameof(iOSLifecycle.WillTerminate));
|
||||
.WillTerminate((app) =>{
|
||||
LogEvent(nameof(iOSLifecycle.WillTerminate));
|
||||
|
||||
IPlatformApplication.Current.Services.GetRequiredService<HostedServiceInitializer>().Dispose();
|
||||
}
|
||||
));
|
||||
|
||||
}
|
||||
));
|
||||
#endif
|
||||
static bool LogEvent(string eventName, string? type = null)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine(
|
||||
$"Lifecycle event: {eventName}{(type == null ? string.Empty : $" ({type})")}");
|
||||
return true;
|
||||
}
|
||||
});
|
||||
#if DEBUG
|
||||
builder.Services.AddBlazorWebViewDeveloperTools();
|
||||
builder.Services.AddDangerousSSLSettingsForDev();
|
||||
#endif
|
||||
|
||||
var app = builder.Build();
|
||||
Services = app.Services;
|
||||
return app;
|
||||
return builder.Build();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,20 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:supportsRtl="true">
|
||||
<service android:name=".HubConnectionForegroundService" android:enabled="true" android:exported="false"/>
|
||||
</application>
|
||||
<application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:supportsRtl="true"></application>
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.SENDTO" />
|
||||
<data android:scheme="mailto" />
|
||||
</intent>
|
||||
</queries>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
|
||||
@ -22,8 +16,6 @@
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.READ_LOGS" />
|
||||
<uses-permission android:name="android.permission.NFC" />
|
||||
<uses-feature android:name="android.hardware.nfc" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
|
||||
</manifest>
|
||||
</manifest>
|
||||
@ -1,61 +0,0 @@
|
||||
using _Microsoft.Android.Resource.Designer;
|
||||
using Android.App;
|
||||
using Android.Content;
|
||||
using Android.Content.PM;
|
||||
using Android.OS;
|
||||
using BTCPayApp.Core.BTCPayServer;
|
||||
|
||||
namespace BTCPayApp.Maui;
|
||||
|
||||
[Service(Enabled = true, ForegroundServiceType = ForegroundService.TypeDataSync)]
|
||||
public class HubConnectionForegroundService : Service
|
||||
{
|
||||
private BTCPayConnectionManager _connectionManager;
|
||||
private const string ChannelId = "HubConnectionForegroundService";
|
||||
|
||||
public override StartCommandResult OnStartCommand(Intent intent, StartCommandFlags flags, int startId)
|
||||
{
|
||||
CreateNotificationChannel();
|
||||
StartForeground(1, BuildNotification());
|
||||
|
||||
_connectionManager = MauiProgram.Services.GetRequiredService<BTCPayConnectionManager>();
|
||||
_connectionManager.RunningInBackground = true;
|
||||
|
||||
return StartCommandResult.Sticky;
|
||||
}
|
||||
|
||||
public override IBinder OnBind(Intent intent) => null;
|
||||
|
||||
public override void OnDestroy()
|
||||
{
|
||||
_connectionManager.RunningInBackground = false;
|
||||
base.OnDestroy();
|
||||
}
|
||||
|
||||
// https://learn.microsoft.com/en-us/dotnet/maui/platform-integration/local-notifications?view=net-maui-8.0&pivots=devices-android
|
||||
private void CreateNotificationChannel()
|
||||
{
|
||||
if (Build.VERSION.SdkInt >= BuildVersionCodes.O)
|
||||
{
|
||||
#pragma warning disable CA1416
|
||||
var channel = new NotificationChannel(ChannelId, "Hub Connection Service", NotificationImportance.Default)
|
||||
{
|
||||
Description = "Keeps the server connection alive."
|
||||
};
|
||||
|
||||
var notificationManager = (NotificationManager)GetSystemService(NotificationService)!;
|
||||
notificationManager.CreateNotificationChannel(channel);
|
||||
#pragma warning restore CA1416
|
||||
}
|
||||
}
|
||||
|
||||
private Notification BuildNotification()
|
||||
{
|
||||
return new Notification.Builder(this, ChannelId)
|
||||
.SetContentTitle("BTCPay Server")
|
||||
.SetContentText("Maintaining server connection...")
|
||||
.SetSmallIcon(ResourceConstant.Drawable.ic_notification)
|
||||
.SetOngoing(true)
|
||||
.Build();
|
||||
}
|
||||
}
|
||||
@ -1,8 +1,5 @@
|
||||
using Android.App;
|
||||
using Android.Content;
|
||||
using Android.Content.PM;
|
||||
using Android.OS;
|
||||
using Plugin.NFC;
|
||||
|
||||
namespace BTCPayApp.Maui;
|
||||
|
||||
@ -11,30 +8,4 @@ namespace BTCPayApp.Maui;
|
||||
ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)]
|
||||
public class MainActivity : MauiAppCompatActivity
|
||||
{
|
||||
protected override void OnCreate(Bundle? savedInstanceState)
|
||||
{
|
||||
CrossNFC.Init(this);
|
||||
base.OnCreate(savedInstanceState);
|
||||
|
||||
#if DEBUG
|
||||
// Enable WebView debugging
|
||||
if (Build.VERSION.SdkInt >= BuildVersionCodes.Kitkat)
|
||||
{
|
||||
Android.Webkit.WebView.SetWebContentsDebuggingEnabled(true); // Fully qualify the WebView class
|
||||
}
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
protected override void OnResume()
|
||||
{
|
||||
base.OnResume();
|
||||
CrossNFC.OnResume();
|
||||
}
|
||||
|
||||
protected override void OnNewIntent(Intent? intent)
|
||||
{
|
||||
base.OnNewIntent(intent);
|
||||
CrossNFC.OnNewIntent(intent);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
</vector>
|
||||
@ -1,42 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M-12.23,20.41l95.24,-0L83,79.93L-12.23,79.93Z"/>
|
||||
<path
|
||||
android:pathData="m45.74,79.07a2.94,2.94 0,0 1,-2.94 -2.94l0,-44.26a2.94,2.94 0,0 1,5.88 -0L48.68,76.13A2.94,2.94 0,0 1,45.74 79.07"
|
||||
android:fillColor="#cedc21"
|
||||
android:strokeColor="#00000000"
|
||||
android:fillType="nonZero"/>
|
||||
<path
|
||||
android:pathData="m45.74,79.07a2.94,2.94 0,0 1,-1.26 -5.6l17.73,-8.42 -18.22,-13.42a2.94,2.94 0,0 1,-0.62 -4.11,2.94 2.94,0 0,1 4.11,-0.62L69.66,63.24A2.94,2.94 45,0 1,69.18 68.26L47,78.79A2.93,2.93 79.23,0 1,45.74 79.07"
|
||||
android:fillColor="#51b13e"
|
||||
android:strokeColor="#00000000"
|
||||
android:fillType="nonZero"/>
|
||||
<path
|
||||
android:pathData="M45.74,61.67A2.94,2.94 0,0 1,44 56.37L62.21,42.94 44.48,34.53a2.94,2.94 135,0 1,-1.4 -3.92c0.7,-1.47 2.45,-2.09 3.92,-1.4L69.18,39.74A2.94,2.94 0,0 1,69.66 44.76L47.49,61.1A2.93,2.93 135,0 1,45.74 61.67"
|
||||
android:fillColor="#cedc21"
|
||||
android:strokeColor="#00000000"
|
||||
android:fillType="nonZero"/>
|
||||
<path
|
||||
android:pathData="m48.68,47.78l0,12.44l8.44,-6.22z"
|
||||
android:fillColor="#1e7a44"
|
||||
android:strokeColor="#00000000"
|
||||
android:fillType="nonZero"/>
|
||||
<path
|
||||
android:pathData="m48.68,56.9l-5.88,-0l0,-14.34l5.88,-0z"
|
||||
android:strokeWidth="0.36"
|
||||
android:fillColor="#ffffff"
|
||||
android:strokeColor="#00000000"
|
||||
android:fillType="nonZero"/>
|
||||
<path
|
||||
android:pathData="M45.74,28.93A2.94,2.94 45,0 0,42.8 31.87L42.8,68.88L48.68,68.88L48.68,31.87A2.94,2.94 135,0 0,45.74 28.93"
|
||||
android:fillColor="#cedc21"
|
||||
android:strokeColor="#00000000"
|
||||
android:fillType="nonZero"/>
|
||||
</group>
|
||||
</vector>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 6.7 KiB |
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
<background android:drawable="@mipmap/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<monochrome android:drawable="@mipmap/ic_launcher_monochrome"/>
|
||||
</adaptive-icon>
|
||||
@ -13,15 +13,24 @@ namespace BTCPayApp.Maui.Services
|
||||
Subject = subject, //"App Log File",
|
||||
Body = body, //"Attached is the log file for review.",
|
||||
BodyFormat = EmailBodyFormat.PlainText,
|
||||
To = [recipient]
|
||||
To = new List<string> { recipient }
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(attachFilePath))
|
||||
if(!string.IsNullOrWhiteSpace(attachFilePath))
|
||||
{
|
||||
message.Attachments?.Add(new EmailAttachment(attachFilePath));
|
||||
}
|
||||
|
||||
await Email.Default.ComposeAsync(message);
|
||||
try
|
||||
{
|
||||
await Email.Default.ComposeAsync(message);
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@ -1,52 +0,0 @@
|
||||
using BTCPayApp.Core.Contracts;
|
||||
using BTCPayApp.Core.Models;
|
||||
using Plugin.NFC;
|
||||
|
||||
public class NfcService : INfcService, IDisposable
|
||||
{
|
||||
public event EventHandler<NfcCardData> OnNfcDataReceived = delegate { };
|
||||
|
||||
public void StartNfc()
|
||||
{
|
||||
if (!CrossNFC.IsSupported)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!CrossNFC.Current.IsEnabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CrossNFC.Current.OnMessageReceived += Current_OnMessageReceived;
|
||||
CrossNFC.Current.StartListening();
|
||||
}
|
||||
public void EndNfc()
|
||||
{
|
||||
CrossNFC.Current.StopListening();
|
||||
Dispose();
|
||||
}
|
||||
|
||||
private void Current_OnMessageReceived(ITagInfo tagInfo)
|
||||
{
|
||||
if (tagInfo == null || tagInfo.Records == null || !tagInfo.Records.Any())
|
||||
{
|
||||
//throw new ArgumentException("No NFC records found in the tag info.");
|
||||
return;
|
||||
}
|
||||
|
||||
var record = tagInfo.Records[0];
|
||||
|
||||
// Pass the raw tag info up - let the consumer decide what to do with it
|
||||
OnNfcDataReceived?.Invoke(this, new NfcCardData
|
||||
{
|
||||
Message = record.Message,
|
||||
Payload = record.Payload
|
||||
});
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
CrossNFC.Current.OnMessageReceived -= Current_OnMessageReceived;
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,6 @@
|
||||
using BTCPayApp.Core.Contracts;
|
||||
using BTCPayApp.Core.Data;
|
||||
using BTCPayApp.Core.Helpers;
|
||||
using BTCPayApp.Maui.Services;
|
||||
using Plugin.Fingerprint;
|
||||
|
||||
@ -14,7 +16,6 @@ public static class StartupExtensions
|
||||
serviceCollection.AddSingleton(CrossFingerprint.Current);
|
||||
serviceCollection.AddSingleton<HostedServiceInitializer>();
|
||||
serviceCollection.AddSingleton<IMauiInitializeService, HostedServiceInitializer>();
|
||||
serviceCollection.AddScoped<IEmailService, EmailService>();
|
||||
|
||||
return serviceCollection;
|
||||
}
|
||||
|
||||
@ -12,7 +12,6 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Photino.Blazor;
|
||||
using Photino.NET;
|
||||
using Size = System.Drawing.Size;
|
||||
|
||||
namespace BTCPayApp.Photino;
|
||||
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
using BTCPayApp.Core.Contracts;
|
||||
using BTCPayApp.Core.Extensions;
|
||||
using BTCPayApp.Desktop;
|
||||
using BTCPayApp.Server.Services;
|
||||
using BTCPayApp.UI;
|
||||
using Serilog;
|
||||
|
||||
@ -19,8 +17,6 @@ builder.Services.ConfigureBTCPayAppCore();
|
||||
builder.Services.AddDangerousSSLSettingsForDev();
|
||||
#endif
|
||||
|
||||
builder.Services.AddSingleton<INfcService, NfcService>();
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
var app = builder.Build();
|
||||
if (!app.Environment.IsDevelopment())
|
||||
|
||||
@ -1,23 +0,0 @@
|
||||
using BTCPayApp.Core.Contracts;
|
||||
using BTCPayApp.Core.Models;
|
||||
|
||||
namespace BTCPayApp.Server.Services
|
||||
{
|
||||
public class NfcService : INfcService
|
||||
{
|
||||
public event EventHandler<NfcCardData> OnNfcDataReceived = delegate { };
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
|
||||
public void EndNfc()
|
||||
{
|
||||
}
|
||||
|
||||
public void StartNfc()
|
||||
{
|
||||
// NFC for web is supported within the btcpayserver iframe, so we dont need to implement NFC support here.
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2,11 +2,14 @@
|
||||
@using BTCPayApp.UI.Features
|
||||
@using BTCPayApp.UI.Pages
|
||||
@using BTCPayApp.Core.Contracts
|
||||
@using BTCPayApp.Core.Services
|
||||
@using BTCPayApp.UI.Pages.SignedOut
|
||||
@using Serilog.Events
|
||||
@inherits Fluxor.Blazor.Web.Components.FluxorComponent
|
||||
@inject IDispatcher Dispatcher
|
||||
@inject IAccountManager AccountManager
|
||||
@inject ConfigProvider ConfigProvider
|
||||
@inject LoggingService LoggingService
|
||||
|
||||
<PageTitle>BTCPay Server</PageTitle>
|
||||
<Fluxor.Blazor.Web.StoreInitializer />
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -5,18 +5,16 @@
|
||||
<div class="accordion-item">
|
||||
<div id="channel-@Channel.Id" class="d-flex flex-column gap-2 accordion-header">
|
||||
<div class="d-flex align-items-center justify-content-between gap-2">
|
||||
<div class="me-auto">
|
||||
<TruncateCenter Text="@Channel.Id" Padding="10" class="fw-semibold me-auto"/>
|
||||
@if (!string.IsNullOrEmpty(Channel.CounterpartyLabel))
|
||||
{
|
||||
<div class="text-muted">@Channel.CounterpartyLabel</div>
|
||||
}
|
||||
</div>
|
||||
<TruncateCenter Text="@Channel.Id" Padding="10" class="fw-semibold me-auto"/>
|
||||
@if (!string.IsNullOrEmpty(Channel.CounterpartyLabel))
|
||||
{
|
||||
<span class="me-auto">@Channel.CounterpartyLabel</span>
|
||||
}
|
||||
@if (Loading)
|
||||
{
|
||||
<LoadingIndicator/>
|
||||
}
|
||||
<span class="btcpay-status btcpay-status--@ChannelState"></span>
|
||||
<span class="btcpay-status btcpay-status--@(Channel.Active ? "enabled" : "pending")"></span>
|
||||
<button class="accordion-button collapsed w-auto p-0" type="button" data-bs-toggle="collapse" data-bs-target="#peer-@Channel.Id-details" aria-expanded="false" aria-controls="peer-@Channel.Id-details">
|
||||
<Icon Symbol="caret-down"/>
|
||||
</button>
|
||||
@ -26,26 +24,17 @@
|
||||
var inbound = (long)Channel.CapacityInbound.ToUnit(LightMoneyUnit.Satoshi);
|
||||
var outbound = (long)Channel.CapacityOutbound.ToUnit(LightMoneyUnit.Satoshi);
|
||||
var ourPerc = (int)(outbound * 100 / (inbound + outbound));
|
||||
<div>
|
||||
<div class="progress w-100 mb-1" role="progressbar" aria-label="Channel Capacity" aria-valuenow="@ourPerc" aria-valuemin="0" aria-valuemax="100">
|
||||
<div class="progress-bar progress-bar-@(ChannelState)" style="width:@(ourPerc)%"></div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center justify-content-between gap-2">
|
||||
<AmountDisplay Sats="@outbound" Currency="@CurrencyDisplay.SATS" />
|
||||
<AmountDisplay Sats="@inbound" Currency="@CurrencyDisplay.SATS" />
|
||||
</div>
|
||||
<div class="d-flex align-items-center justify-content-between gap-2">
|
||||
<AmountDisplay Sats="@outbound" Currency="@CurrencyDisplay.SATS" />
|
||||
<AmountDisplay Sats="@inbound" Currency="@CurrencyDisplay.SATS" />
|
||||
</div>
|
||||
<div class="progress w-100" role="progressbar" aria-label="Channel Capacity" aria-valuenow="@ourPerc" aria-valuemin="0" aria-valuemax="100">
|
||||
<div class="progress-bar bg-success" style="width:@(ourPerc)%"></div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div id="peer-@Channel.Id-details" class="accordion-collapse collapse" aria-labelledby="peer-@Channel.Id">
|
||||
<div class="pt-3">
|
||||
@if (!string.IsNullOrEmpty(Channel.CounterpartyLabel))
|
||||
{
|
||||
<div class="form-floating">
|
||||
<TruncateCenter Text="@Channel.CounterpartyLabel" Padding="20" Elastic="true" class="form-control-plaintext"/>
|
||||
<label>Counterparty</label>
|
||||
</div>
|
||||
}
|
||||
<div class="pt-2">
|
||||
@if (!string.IsNullOrEmpty(Channel.CounterpartyNodeId))
|
||||
{
|
||||
<div class="form-floating">
|
||||
@ -60,46 +49,32 @@
|
||||
<label>Funding Transaction</label>
|
||||
</div>
|
||||
}
|
||||
<dl class="mt-2 mb-0">
|
||||
<div class="align-items-center">
|
||||
<dt>@(Channel.Connected ? "Connected" : "Not connected")</dt>
|
||||
<span class="btcpay-status btcpay-status--@(Channel.Connected ? "enabled" : "disabled")"></span>
|
||||
<dl>
|
||||
<div>
|
||||
<dt>State:</dt>
|
||||
<dd>@Channel.State</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Connected:</dt>
|
||||
<dd>@Channel.Connected</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Usable:</dt>
|
||||
<dd>@Channel.Usable</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Ready:</dt>
|
||||
<dd>@Channel.Ready</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Announced:</dt>
|
||||
<dd>@Channel.Announced</dd>
|
||||
</div>
|
||||
@if (Channel is { Connected: true, Ready: not null })
|
||||
{
|
||||
<div class="align-items-center">
|
||||
<dt class="text-wrap">@(Channel.Ready.Value ? "Ready" : $"Not ready: {Channel.Confirmations} of {Channel.ConfirmationsRequired} confirmations")</dt>
|
||||
<span class="btcpay-status btcpay-status--@(Channel.Ready.Value ? "enabled" : "pending")"></span>
|
||||
</div>
|
||||
@if (Channel is { Connected: true, Ready: true, Usable: not null })
|
||||
{
|
||||
<div class="align-items-center">
|
||||
<dt>@(Channel.Usable.Value ? "Usable" : "Not usable")</dt>
|
||||
<span class="btcpay-status btcpay-status--@(Channel.Usable.Value ? "enabled" : "pending")"></span>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(Channel.State) || Channel.Announced.HasValue)
|
||||
{
|
||||
<div class="align-items-center">
|
||||
<dt>State:</dt>
|
||||
<dd class="d-inline-flex flex-wrap gap-1">
|
||||
@if (!string.IsNullOrEmpty(Channel.State))
|
||||
{
|
||||
<span>@Channel.State</span>
|
||||
}
|
||||
@if (Channel.Announced.HasValue)
|
||||
{
|
||||
<span>(@(Channel.Announced.Value ? "Announced" : "Unannounced"))</span>
|
||||
}
|
||||
</dd>
|
||||
</div>
|
||||
}
|
||||
</dl>
|
||||
@if (Channel.Active && OnCloseChannel.HasDelegate)
|
||||
{
|
||||
<div class="d-grid d-sm-flex flex-wrap gap-3 buttons mt-3">
|
||||
<button type="button" class="btn btn-outline-danger btn-sm" @onclick="CloseChannel">@(Channel.Connected ? "Close" : "Force-close (not connected)")</button>
|
||||
<button type="button" class="btn btn-outline-danger btn-sm" @onclick="CloseChannel">Close</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@ -116,10 +91,6 @@
|
||||
[Parameter]
|
||||
public bool Loading { get; set; }
|
||||
|
||||
private string ChannelState => Channel.Active
|
||||
? Channel.Ready is true ? "enabled" : "pending"
|
||||
: Channel.Connected ? "pending" : "disabled";
|
||||
|
||||
private async Task CloseChannel()
|
||||
{
|
||||
if (OnCloseChannel.HasDelegate)
|
||||
|
||||
@ -1,12 +0,0 @@
|
||||
.progress {
|
||||
background-color: var(--btcpay-neutral-400);
|
||||
}
|
||||
|
||||
.progress-bar-enabled {
|
||||
background-color: var(--btcpay-success);
|
||||
}
|
||||
|
||||
.progress-bar-pending,
|
||||
.progress-bar-disabled {
|
||||
background-color: var(--btcpay-neutral-500);
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
::deep .form-control[readonly] {
|
||||
background-color: var(--btcpay-form-bg-addon);
|
||||
border-color: var(--btcpay-form-bg-addon);
|
||||
pointer-events: none;
|
||||
}
|
||||
@ -1,8 +1,4 @@
|
||||
@using BTCPayApp.Core.Auth
|
||||
@using BTCPayApp.UI.Features
|
||||
@using BTCPayServer.Client.Models
|
||||
@inject IAccountManager AccountManager
|
||||
@inject IDispatcher Dispatcher
|
||||
<div @attributes="InputAttributes" class="@CssClass">
|
||||
@if (Invoices is not null)
|
||||
{
|
||||
@ -48,21 +44,4 @@
|
||||
public Dictionary<string, object>? InputAttributes { get; set; }
|
||||
|
||||
private string CssClass => $"invoice-list {(InputAttributes?.ContainsKey("class") is true ? InputAttributes["class"] : "")}".Trim();
|
||||
|
||||
private string? StoreId => AccountManager.CurrentStore?.Id;
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(StoreId))
|
||||
{
|
||||
Dispatcher.Dispatch(new StoreState.FetchInvoices(StoreId));
|
||||
}
|
||||
}
|
||||
}
|
||||
private async void OnStateChanged(object? sender, EventArgs e)
|
||||
{
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,7 +2,6 @@
|
||||
@using System.Globalization
|
||||
@using System.Text.Json
|
||||
@using System.Text.Json.Nodes
|
||||
@using BTCPayApp.Core.Models
|
||||
@using BTCPayServer.Client
|
||||
@using BTCPayServer.Client.Models
|
||||
@inject IJSRuntime JS
|
||||
@ -98,21 +97,18 @@
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-center">
|
||||
<button class="btn btn-lg btn-primary mx-3" type="submit" disabled="@(IsSubmitting || amount == 0)" id="pay-button" @onclick="HandleSubmit">
|
||||
@if (IsSubmitting)
|
||||
{
|
||||
<div class="spinner-border spinner-border-sm" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>Charge</span>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
<button class="btn btn-lg btn-primary mx-3" type="submit" disabled="@(IsSubmitting || amount == 0)" id="pay-button" @onclick="HandleSubmit">
|
||||
@if (IsSubmitting)
|
||||
{
|
||||
<div class="spinner-border spinner-border-sm" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>Charge</span>
|
||||
}
|
||||
</button>
|
||||
|
||||
@if (CanAccessRecentTransactions)
|
||||
{
|
||||
@ -252,7 +248,7 @@
|
||||
</div>
|
||||
|
||||
@code {
|
||||
#nullable enable
|
||||
#nullable enable
|
||||
[Parameter, EditorRequired]
|
||||
public string StoreId { get; set; } = null!;
|
||||
[Parameter, EditorRequired]
|
||||
@ -278,7 +274,7 @@
|
||||
[Parameter]
|
||||
public EventCallback LoadRecentTransactions { get; set; }
|
||||
[Parameter]
|
||||
public EventCallback<Core.Models.CreatePosInvoiceRequest> CreateInvoice { get; set; }
|
||||
public EventCallback<CreatePosInvoiceRequest> CreateInvoice { get; set; }
|
||||
[Parameter]
|
||||
public IEnumerable<RecentTransaction>? RecentTransactions { get; set; }
|
||||
[Parameter]
|
||||
@ -440,6 +436,8 @@
|
||||
{
|
||||
var data = new JsonObject
|
||||
{
|
||||
["subTotal"] = GetAmount(),
|
||||
["total"] = GetTotal(),
|
||||
["cart"] = JsonValue.Create(Model.Cart)
|
||||
};
|
||||
|
||||
@ -669,12 +667,16 @@
|
||||
StateHasChanged();
|
||||
if (CreateInvoice.HasDelegate)
|
||||
{
|
||||
var req = new Core.Models.CreatePosInvoiceRequest
|
||||
var req = new CreatePosInvoiceRequest
|
||||
{
|
||||
Cart = Model.Cart,
|
||||
DiscountPercent = Model.DiscountPercent,
|
||||
TipPercent = Model.TipPercent,
|
||||
Tip = GetTip(),
|
||||
Amounts = GetAmounts(),
|
||||
DiscountAmount = GetDiscount(),
|
||||
Subtotal = GetAmount(),
|
||||
Total = GetTotal(),
|
||||
PosData = GetData()
|
||||
};
|
||||
await CreateInvoice.InvokeAsync(req);
|
||||
|
||||
@ -1,47 +1,47 @@
|
||||
@using BTCPayApp.Core.BTCPayServer
|
||||
@using BTCPayApp.Core.Wallet
|
||||
|
||||
@if (NodeState == LightningNodeState.Error)
|
||||
@if (State is LightningNodeState.Error)
|
||||
{
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<h4 class="alert-heading">Lightning Node @(Status)</h4>
|
||||
<p class="mb-0">
|
||||
There was an error with the lightning node. Please
|
||||
<NavLink href="@Routes.AppLogs">check the app logs</NavLink>
|
||||
and try again later.
|
||||
</p>
|
||||
<h4 class="alert-heading">Lightning Node failure</h4>
|
||||
<p class="mb-0">There was an error with the lightning node. Please try again later.</p>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
else if (State is LightningNodeState.Stopped)
|
||||
{
|
||||
<div class="alert alert-warning" role="alert">
|
||||
<h4 class="alert-heading">Lightning Node stopped</h4>
|
||||
<p class="mb-0">This instance of the lightning node is not running currently.</p>
|
||||
</div>
|
||||
}
|
||||
else if (State is LightningNodeState.Inactive)
|
||||
{
|
||||
<div class="alert alert-info" role="alert">
|
||||
<h4 class="alert-heading">Lightning Node @(Status)</h4>
|
||||
<p class="mb-0">
|
||||
@if (ConnectionState == BTCPayConnectionState.ConnectedAsSecondary)
|
||||
{
|
||||
<span>This device is currently connected as an additional device for communication with the BTCPay Server.</span>
|
||||
}
|
||||
<span>The lightning node is currently @(Status).</span>
|
||||
</p>
|
||||
<h4 class="alert-heading">Lightning Node inactive</h4>
|
||||
<p class="mb-0">There seems to be another instance of this lightning node running. This instance is inactive.</p>
|
||||
</div>
|
||||
}
|
||||
else if (State is not LightningNodeState.Loaded)
|
||||
{
|
||||
<div class="alert alert-info" role="alert">
|
||||
<h4 class="alert-heading">Lightning Node loading</h4>
|
||||
<p class="mb-0">The lightning node is currently @Status.</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
[Parameter, EditorRequired]
|
||||
public LightningNodeState? NodeState { get; set; }
|
||||
|
||||
[Parameter, EditorRequired]
|
||||
public BTCPayConnectionState? ConnectionState { get; set; }
|
||||
public LightningNodeState? State { get; set; }
|
||||
|
||||
private string Status =>
|
||||
NodeState switch
|
||||
State switch
|
||||
{
|
||||
LightningNodeState.Init => "initializing",
|
||||
LightningNodeState.NotConfigured => "not configured",
|
||||
LightningNodeState.WaitingForConnection => ConnectionState == BTCPayConnectionState.ConnectedAsSecondary ? "inactive" : "waiting for connection",
|
||||
LightningNodeState.WaitingForConnection => "waiting for connection",
|
||||
LightningNodeState.Loaded => "loaded",
|
||||
LightningNodeState.Stopped => "stopped",
|
||||
LightningNodeState.Error => "in failure mode",
|
||||
_ => ConnectionState == BTCPayConnectionState.Syncing ? "syncing" : "loading"
|
||||
LightningNodeState.Inactive => "inactive",
|
||||
_ => "loading"
|
||||
};
|
||||
}
|
||||
|
||||
@ -16,13 +16,6 @@
|
||||
</div>
|
||||
<div id="peer-@Peer.NodeId-details" class="accordion-collapse collapse" aria-labelledby="peer-@Peer.NodeId">
|
||||
<div class="pt-2">
|
||||
@if (OnUpdatePeer.HasDelegate)
|
||||
{
|
||||
<div class="form-group mb-2">
|
||||
<label for="Label" class="form-label">Label</label>
|
||||
<InputText @bind-Value="Label" @bind-Value:after="UpdatePeerLabel" id="Label" class="form-control"/>
|
||||
</div>
|
||||
}
|
||||
<div class="form-floating">
|
||||
<TruncateCenter Text="@Peer.NodeId" Padding="10" Copy="true" Elastic="true" class="form-control-plaintext"/>
|
||||
<label>Node ID</label>
|
||||
@ -76,16 +69,6 @@
|
||||
[Parameter]
|
||||
public bool Loading { get; set; }
|
||||
|
||||
private string Label
|
||||
{
|
||||
get => Peer.Info?.Label ?? string.Empty;
|
||||
set
|
||||
{
|
||||
Peer.Info ??= new PeerInfo();
|
||||
Peer.Info.Label = value;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ConnectPeer()
|
||||
{
|
||||
if (OnConnectPeer.HasDelegate)
|
||||
@ -106,13 +89,4 @@
|
||||
await OnUpdatePeer.InvokeAsync(Peer);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UpdatePeerLabel()
|
||||
{
|
||||
if (OnUpdatePeer.HasDelegate)
|
||||
{
|
||||
Peer.Info = (Peer.Info ?? new PeerInfo()) with { Label = Label };
|
||||
await OnUpdatePeer.InvokeAsync(Peer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -74,7 +74,7 @@
|
||||
clss += $" badge-{(status == "unconfirmed" ? "pending" : "settled")}";
|
||||
break;
|
||||
case TransactionPaymentMethod.Lightning:
|
||||
clss += $" badge-{(status == "failed" ? "invalid" : status)}";
|
||||
clss += $" badge-{status}";
|
||||
break;
|
||||
}
|
||||
return clss;
|
||||
|
||||
@ -67,8 +67,8 @@
|
||||
|
||||
private void EditContextOnOnFieldChanged(object? sender, FieldChangedEventArgs e)
|
||||
{
|
||||
if (EditContext?.GetValidationMessages(e.FieldIdentifier).Any() is false)
|
||||
MessageStore?.Clear(e.FieldIdentifier);
|
||||
if (!EditContext?.GetValidationMessages(e.FieldIdentifier).Any() is true) return;
|
||||
MessageStore?.Clear(e.FieldIdentifier);
|
||||
EditContext?.NotifyValidationStateChanged();
|
||||
}
|
||||
|
||||
@ -77,11 +77,6 @@
|
||||
EditContext?.NotifyFieldChanged(FieldIdentifier.Create(field));
|
||||
}
|
||||
|
||||
public void NotifyValidationStateChanged()
|
||||
{
|
||||
EditContext?.NotifyValidationStateChanged();
|
||||
}
|
||||
|
||||
private void EditContextOnOnValidationStateChanged(object? sender, ValidationStateChangedEventArgs e)
|
||||
{
|
||||
StateHasChanged();
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
@using BTCPayServer.Client.Models
|
||||
@implements IDisposable
|
||||
@inject IJSRuntime JS
|
||||
@inject DisplayFormatter DisplayFormatter
|
||||
|
||||
<div @attributes="InputAttributes" class="@CssClass">
|
||||
@if (Sats != null)
|
||||
|
||||
@ -1,46 +0,0 @@
|
||||
@using BTCPayApp.Core.BTCPayServer
|
||||
@using BTCPayApp.Core.Wallet
|
||||
|
||||
@if (WalletState == OnChainWalletState.Error)
|
||||
{
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<h4 class="alert-heading">Onchain Wallet @(Status)</h4>
|
||||
<p class="mb-0">
|
||||
There was an error with the onchain wallet. Please
|
||||
<NavLink href="@Routes.AppLogs">check the app logs</NavLink>
|
||||
and try again later.
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="alert alert-info" role="alert">
|
||||
<h4 class="alert-heading">Onchain Wallet @(Status)</h4>
|
||||
<p class="mb-0">
|
||||
@if (ConnectionState == BTCPayConnectionState.ConnectedAsSecondary)
|
||||
{
|
||||
<span>This device is currently connected as an additional device for communication with the BTCPay Server.</span>
|
||||
}
|
||||
<span>The onchain wallet is currently @(Status).</span>
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
[Parameter, EditorRequired]
|
||||
public OnChainWalletState? WalletState { get; set; }
|
||||
|
||||
[Parameter, EditorRequired]
|
||||
public BTCPayConnectionState? ConnectionState { get; set; }
|
||||
|
||||
private string Status =>
|
||||
WalletState switch
|
||||
{
|
||||
OnChainWalletState.Init => "initializing",
|
||||
OnChainWalletState.NotConfigured => "not configured",
|
||||
OnChainWalletState.WaitingForConnection => ConnectionState == BTCPayConnectionState.ConnectedAsSecondary ? "inactive" : "waiting for connection",
|
||||
OnChainWalletState.Loaded => "loaded",
|
||||
OnChainWalletState.Error => "in failure mode",
|
||||
_ => ConnectionState == BTCPayConnectionState.Syncing ? "syncing" : "loading"
|
||||
};
|
||||
}
|
||||
@ -10,10 +10,12 @@ public class LoginModel
|
||||
[Required]
|
||||
public string? Uri { get; set; }
|
||||
|
||||
[Required, EmailAddress]
|
||||
[Required]
|
||||
[EmailAddress]
|
||||
public string? Email { get; set; }
|
||||
|
||||
[Required, DataType(DataType.Password)]
|
||||
[Required]
|
||||
[DataType(DataType.Password)]
|
||||
public string? Password { get; set; }
|
||||
|
||||
[RequiredIf(nameof(RequireTwoFactor), true)]
|
||||
|
||||
@ -1,87 +1,25 @@
|
||||
@attribute [Route(Routes.Checkout)]
|
||||
@using BTCPayApp.Core
|
||||
@using BTCPayApp.Core.Auth
|
||||
@using BTCPayApp.Core.BTCPayServer
|
||||
@using BTCPayApp.Core.Contracts
|
||||
@using BTCPayApp.Core.Models
|
||||
@inject IJSRuntime JS
|
||||
@inject IAccountManager AccountManager
|
||||
@inject INfcService NfcService
|
||||
@inject NavigationManager NavigationManager
|
||||
|
||||
@inherits Fluxor.Blazor.Web.Components.FluxorComponent
|
||||
|
||||
<PageTitle>Checkout</PageTitle>
|
||||
|
||||
<iframe id="AppCheckout" name="checkout" allowfullscreen src="@CheckoutUrl" @onload="OnIframeLoad"></iframe>
|
||||
|
||||
@if (!_iframeLoaded)
|
||||
{
|
||||
<section class="loading-container">
|
||||
<LoadingIndicator Size="lg" />
|
||||
<LoadingIndicator Size="lg"/>
|
||||
<div class="fs-4">Loading</div>
|
||||
</section>
|
||||
}
|
||||
|
||||
@if (_toast.IsVisible)
|
||||
{
|
||||
<div class="toast-container @(_toast.IsError ? "toast-error" : "toast-success")">
|
||||
<div class="toast-message">@_toast.Message</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<style>
|
||||
.toast-container {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 9999;
|
||||
padding: 12px 24px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
max-width: 90%;
|
||||
min-width: 200px;
|
||||
text-align: center;
|
||||
animation: slideUp 0.3s ease-out;
|
||||
}
|
||||
|
||||
.toast-success {
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
border: 1px solid #1e7e34;
|
||||
}
|
||||
|
||||
.toast-error {
|
||||
background-color: #dc3545;
|
||||
color: white;
|
||||
border: 1px solid #c82333;
|
||||
}
|
||||
|
||||
.toast-message {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
@@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-50%) translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<iframe id="AppCheckout" name="checkout" allow="clipboard-read;clipboard-write" allowfullscreen src="@CheckoutUrl" @onload="OnIframeLoad"></iframe>
|
||||
|
||||
@code {
|
||||
[Parameter, EditorRequired] public string? InvoiceId { get; set; }
|
||||
[Parameter, EditorRequired]
|
||||
public string? InvoiceId { get; set; }
|
||||
|
||||
private bool _iframeLoaded;
|
||||
private bool _scanInProgress;
|
||||
private Toast _toast = new();
|
||||
|
||||
private string BaseUri => AccountManager.Account!.BaseUri;
|
||||
private string? CheckoutUrl => string.IsNullOrEmpty(InvoiceId) ? null : $"{BaseUri}i/{InvoiceId}";
|
||||
@ -90,126 +28,5 @@
|
||||
{
|
||||
_iframeLoaded = true;
|
||||
await JS.InvokeVoidAsync("Interop.setContext", "#AppCheckout", BaseUri);
|
||||
|
||||
NfcService.OnNfcDataReceived += OnNfcDataReceived;
|
||||
NfcService.StartNfc();
|
||||
_toast.OnUpdated = StateHasChanged;
|
||||
}
|
||||
|
||||
private async void OnNfcDataReceived(object? sender, NfcCardData record)
|
||||
{
|
||||
if (_scanInProgress)
|
||||
return;
|
||||
|
||||
_scanInProgress = true;
|
||||
try
|
||||
{
|
||||
if (record == null || record.Message == null)
|
||||
{
|
||||
_toast.ShowError("No NFC data found");
|
||||
return;
|
||||
}
|
||||
|
||||
var message = record.Message;
|
||||
if (string.IsNullOrEmpty(message))
|
||||
{
|
||||
_toast.ShowError("Empty NFC message");
|
||||
return;
|
||||
}
|
||||
|
||||
var btcPayClient = new BTCPayAppClient(BaseUri);
|
||||
var req = new SubmitLnUrlRequest
|
||||
{
|
||||
InvoiceId = InvoiceId,
|
||||
Lnurl = message
|
||||
};
|
||||
|
||||
_toast.ShowSuccess("Submitting NFC data to Server", 5000);
|
||||
|
||||
var result = await btcPayClient.SubmitLNURLWithdrawForInvoice(req);
|
||||
if (result == null)
|
||||
{
|
||||
_toast.Hide();
|
||||
NfcService.EndNfc();
|
||||
NfcService.OnNfcDataReceived -= OnNfcDataReceived;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
try
|
||||
{
|
||||
var errorFromServer = ex.Message.Split("\"")[1].Trim('"');
|
||||
_toast.ShowError("ERROR: " + errorFromServer);
|
||||
}
|
||||
catch
|
||||
{
|
||||
_toast.ShowError("NFC call to server ERROR");
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_scanInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
NfcService.OnNfcDataReceived -= OnNfcDataReceived;
|
||||
}
|
||||
|
||||
public class Toast
|
||||
{
|
||||
private CancellationTokenSource? _cts;
|
||||
|
||||
public bool IsVisible { get; private set; }
|
||||
public bool IsError { get; private set; }
|
||||
public string Message { get; private set; } = string.Empty;
|
||||
|
||||
public Action? OnUpdated { get; set; }
|
||||
|
||||
public void ShowSuccess(string message, int delayTime = 3000)
|
||||
{
|
||||
Show(message, false);
|
||||
}
|
||||
|
||||
public void ShowError(string message, int delayTime = 3000)
|
||||
{
|
||||
Show(message, true);
|
||||
}
|
||||
|
||||
private void Show(string message, bool isError, int delayTime = 3000)
|
||||
{
|
||||
_cts?.Cancel();
|
||||
_cts = new CancellationTokenSource();
|
||||
|
||||
Message = message;
|
||||
IsError = isError;
|
||||
IsVisible = true;
|
||||
OnUpdated?.Invoke();
|
||||
|
||||
var token = _cts.Token;
|
||||
_ = HideAfterDelayAsync(token);
|
||||
}
|
||||
|
||||
private async Task HideAfterDelayAsync(CancellationToken token, int delayTime = 3000)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(delayTime, token);
|
||||
if (!token.IsCancellationRequested)
|
||||
{
|
||||
IsVisible = false;
|
||||
OnUpdated?.Invoke();
|
||||
}
|
||||
}
|
||||
catch (TaskCanceledException) { }
|
||||
}
|
||||
|
||||
public void Hide()
|
||||
{
|
||||
_cts?.Cancel();
|
||||
IsVisible = false;
|
||||
OnUpdated?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,28 +1,15 @@
|
||||
.container {
|
||||
max-width: 560px;
|
||||
padding-top: var(--btcpay-space-l);
|
||||
padding-bottom: var(--btcpay-space-l);
|
||||
max-width: 560px;
|
||||
padding-top: var(--btcpay-space-l);
|
||||
padding-bottom: var(--btcpay-space-l);
|
||||
}
|
||||
|
||||
iframe {
|
||||
position: fixed;
|
||||
z-index: 1;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%; /* fallback for older webviews */
|
||||
height: calc(100% - var(--navbar-bottom-height));
|
||||
position: fixed;
|
||||
z-index: 1;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%; /* fallback for older webviews */
|
||||
height: calc(100% - var(--navbar-bottom-height));
|
||||
}
|
||||
|
||||
|
||||
.section-complete ::deep .checkout-complete {
|
||||
width: 110px; /* Increase size */
|
||||
height: 100px;
|
||||
fill: var(--btcpay-primary) !important; /* Change fill color */
|
||||
stroke: var(--btcpay-primary) !important; /* Change stroke color */
|
||||
/*fill: var(--btcpay-primary);*/
|
||||
}
|
||||
|
||||
.section-complete {
|
||||
height: 80vh;
|
||||
}
|
||||
@ -104,9 +104,8 @@
|
||||
_errorMessage = null;
|
||||
_data = await AccountManager.GetClient().GetCreateStore();
|
||||
|
||||
Model.Name = _data?.Name;
|
||||
Model.DefaultCurrency = _data?.DefaultCurrency;
|
||||
Model.PreferredExchange = _data?.PreferredExchangeId;
|
||||
Model.PreferredExchange = _data?.RecommendedExchangeId;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@ -144,12 +143,11 @@
|
||||
var rateConfig = new StoreRateConfiguration { PreferredSource = Model.PreferredExchange };
|
||||
await AccountManager.GetClient().UpdateStoreRateConfiguration(store.Id, rateConfig);
|
||||
|
||||
// refresh store info and set id of the new store
|
||||
// refresh store info and set id of new store
|
||||
await AccountManager.CheckAuthenticated(true);
|
||||
var result = await AccountManager.SetCurrentStoreId(store.Id);
|
||||
if (!result.Succeeded)
|
||||
{
|
||||
_sending = false;
|
||||
_errorMessage = result.Messages != null
|
||||
? string.Join(",", result.Messages)
|
||||
: "Store creation failed.";
|
||||
@ -159,9 +157,11 @@
|
||||
catch (Exception e)
|
||||
{
|
||||
_errorMessage = e.Message;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_sending = false;
|
||||
}
|
||||
// on success, keep _sending = true to show the loading indicator until the redirect occurs
|
||||
}
|
||||
|
||||
private class CreateStoreModel
|
||||
|
||||
@ -22,7 +22,7 @@
|
||||
<section class="container py-4">
|
||||
@if (!string.IsNullOrEmpty(_successMessage))
|
||||
{
|
||||
<Alert Type="success" Margin="0" Dismissible class="mb-4">@_successMessage</Alert>
|
||||
<Alert Type="success" Margin="0" Dismissible>@_successMessage</Alert>
|
||||
}
|
||||
@if (string.IsNullOrEmpty(StoreId))
|
||||
{
|
||||
@ -36,7 +36,7 @@
|
||||
</div>
|
||||
<WalletOverview Sats="@TotalBalance" Histogram="@Histogram" Currency="@DisplayCurrency" Rate="@Rate"
|
||||
Error="@BalanceError" Loading="@BalanceLoading" OnBalanceClick="ToggleDisplayCurrency"
|
||||
class="mt-4 mb-3"/>
|
||||
class="my-3"/>
|
||||
<div class="btn-group w-100 justify-content-center" role="group" aria-label="Period">
|
||||
<InputRadioGroup Name="HistogramPeriod" @bind-Value="@HistogramPeriod">
|
||||
<InputRadio Name="HistogramPeriod" Value="HistogramType.Day" class="btn-check" id="BalancePeriodDay"/>
|
||||
@ -53,7 +53,7 @@
|
||||
<label class="btn btn-link" for="BalancePeriodTwoYears">2Y</label>
|
||||
</InputRadioGroup>
|
||||
</div>
|
||||
@if (true || TotalBalance is > 0)
|
||||
@if (TotalBalance is > 0)
|
||||
{
|
||||
<div class="text-center my-5">
|
||||
<NavLink href="@Routes.Withdraw" class="d-inline-flex align-items-center gap-1 fs-5 bg-light rounded-pill px-5 py-2 fw-semibold">
|
||||
@ -79,7 +79,7 @@
|
||||
{
|
||||
<AppSalesStats AppId="@PosAppId" Stats="PosSalesStats" Loading="PosSalesStatsLoading" Error="@PosSalesStatsError" class="mt-5"/>
|
||||
}
|
||||
@if (ShowPosItemStats)
|
||||
@if (PosItemStatsLoading || PosItemStats is not null)
|
||||
{
|
||||
<AppItemStats AppId="@PosAppId" Stats="PosItemStats" Loading="PosItemStatsLoading" Error="@PosItemStatsError" class="mt-5"/>
|
||||
}
|
||||
@ -143,5 +143,4 @@
|
||||
private List<AppItemStats>? PosItemStats => StoreState.Value.PosItemStats?.Data;
|
||||
private string? PosSalesStatsError => StoreState.Value.PosSalesStats?.Error;
|
||||
private string? PosItemStatsError => StoreState.Value.PosItemStats?.Error;
|
||||
private bool ShowPosItemStats => PosItemStats is not null && PosItemStats is not [{ Title: "Keypad" }];
|
||||
}
|
||||
|
||||
@ -36,10 +36,6 @@
|
||||
{
|
||||
<Alert Type="danger">@Error</Alert>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(_successMessage))
|
||||
{
|
||||
<Alert Type="success">@_successMessage</Alert>
|
||||
}
|
||||
@if (Invoice is not null)
|
||||
{
|
||||
@if (CanCheckout)
|
||||
@ -60,64 +56,45 @@
|
||||
<InvoiceStatusDisplay Invoice="@Invoice"/>
|
||||
</div>
|
||||
|
||||
<div class="invoice-actions mt-3 mb-3">
|
||||
<div class="row g-2">
|
||||
@if (!string.IsNullOrEmpty(ReceiptUrl))
|
||||
{
|
||||
<div class="col-4">
|
||||
<a class="btn btn-outline-secondary btn-sm w-100" href="@ReceiptUrl" rel="noreferrer noopener" target="_blank">Receipt</a>
|
||||
</div>
|
||||
}
|
||||
@if (!Invoice.Archived)
|
||||
{
|
||||
<div class="col-4">
|
||||
<button class="btn btn-outline-secondary btn-sm w-100" @onclick="ToggleArchive">
|
||||
Archive
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 class="mt-4">General Information</h4>
|
||||
<div class="box">
|
||||
<table class="table my-0">
|
||||
<tbody>
|
||||
@if (Invoice.Metadata.TryGetValue("orderId", out var orderId))
|
||||
{
|
||||
<tr>
|
||||
<th>Order Id</th>
|
||||
<td>
|
||||
@if (Invoice.Metadata.TryGetValue("orderUrl", out var orderUrl))
|
||||
{
|
||||
<a href="@orderUrl" rel="noreferrer noopener" target="_blank">@orderId</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>@orderId</span>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@if (Invoice.Metadata.TryGetValue("paymentRequestId", out var paymentRequestId))
|
||||
{
|
||||
<tr>
|
||||
<th>Payment Request Id</th>
|
||||
<td>@paymentRequestId</td>
|
||||
</tr>
|
||||
}
|
||||
@if (Invoice.Metadata.TryGetValue("orderId", out var orderId))
|
||||
{
|
||||
<tr>
|
||||
<th>Created</th>
|
||||
<th>Order Id</th>
|
||||
<td>
|
||||
<DateDisplay DateTimeOffset="@Invoice.CreatedTime"/>
|
||||
@if (Invoice.Metadata.TryGetValue("orderUrl", out var orderUrl))
|
||||
{
|
||||
<a href="@orderUrl" rel="noreferrer noopener" target="_blank">@orderId</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>@orderId</span>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@if (Invoice.Metadata.TryGetValue("paymentRequestId", out var paymentRequestId))
|
||||
{
|
||||
<tr>
|
||||
<th>Expired</th>
|
||||
<td>
|
||||
<DateDisplay DateTimeOffset="@Invoice.ExpirationTime"/>
|
||||
</td>
|
||||
<th>Payment Request Id</th>
|
||||
<td>@paymentRequestId</td>
|
||||
</tr>
|
||||
}
|
||||
<tr>
|
||||
<th>Created</th>
|
||||
<td>
|
||||
<DateDisplay DateTimeOffset="@Invoice.CreatedTime"/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Expired</th>
|
||||
<td>
|
||||
<DateDisplay DateTimeOffset="@Invoice.ExpirationTime"/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@ -134,27 +111,27 @@
|
||||
<div class="box">
|
||||
<table class="table my-0">
|
||||
<tbody>
|
||||
@if (!string.IsNullOrEmpty(itemCode?.ToString()))
|
||||
{
|
||||
<tr>
|
||||
<th>Item code</th>
|
||||
<td>@itemCode</td>
|
||||
</tr>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(itemDesc?.ToString()))
|
||||
{
|
||||
<tr>
|
||||
<th>Item Description</th>
|
||||
<td>@itemDesc</td>
|
||||
</tr>
|
||||
}
|
||||
@if (taxIncluded is not null)
|
||||
{
|
||||
<tr>
|
||||
<th>Tax Included</th>
|
||||
<td>@taxIncluded</td>
|
||||
</tr>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(itemCode?.ToString()))
|
||||
{
|
||||
<tr>
|
||||
<th>Item code</th>
|
||||
<td>@itemCode</td>
|
||||
</tr>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(itemDesc?.ToString()))
|
||||
{
|
||||
<tr>
|
||||
<th>Item Description</th>
|
||||
<td>@itemDesc</td>
|
||||
</tr>
|
||||
}
|
||||
@if (taxIncluded is not null)
|
||||
{
|
||||
<tr>
|
||||
<th>Tax Included</th>
|
||||
<td>@taxIncluded</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@ -173,80 +150,80 @@
|
||||
Invoice.Metadata.TryGetValue("buyerZip", out var buyerZip);
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(buyerName?.ToString()) || !string.IsNullOrEmpty(buyerEmail?.ToString()) ||
|
||||
!string.IsNullOrEmpty(buyerPhone?.ToString()) || !string.IsNullOrEmpty(buyerAddress1?.ToString()) ||
|
||||
!string.IsNullOrEmpty(buyerAddress2?.ToString()) || !string.IsNullOrEmpty(buyerCity?.ToString()) ||
|
||||
!string.IsNullOrEmpty(buyerState?.ToString()) || !string.IsNullOrEmpty(buyerCountry?.ToString()) ||
|
||||
!string.IsNullOrEmpty(buyerZip?.ToString()))
|
||||
!string.IsNullOrEmpty(buyerPhone?.ToString()) || !string.IsNullOrEmpty(buyerAddress1?.ToString()) ||
|
||||
!string.IsNullOrEmpty(buyerAddress2?.ToString()) || !string.IsNullOrEmpty(buyerCity?.ToString()) ||
|
||||
!string.IsNullOrEmpty(buyerState?.ToString()) || !string.IsNullOrEmpty(buyerCountry?.ToString()) ||
|
||||
!string.IsNullOrEmpty(buyerZip?.ToString()))
|
||||
{
|
||||
<h4 class="mt-4">Buyer Information</h4>
|
||||
<div class="box">
|
||||
<table class="table my-0">
|
||||
<tbody>
|
||||
@if (!string.IsNullOrEmpty(buyerName?.ToString()))
|
||||
{
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<td>@buyerName</td>
|
||||
</tr>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(buyerEmail?.ToString()))
|
||||
{
|
||||
<tr>
|
||||
<th>Email</th>
|
||||
<td>
|
||||
<a href="mailto:@buyerEmail">@buyerEmail</a>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(buyerPhone?.ToString()))
|
||||
{
|
||||
<tr>
|
||||
<th>Phone</th>
|
||||
<td>@buyerPhone</td>
|
||||
</tr>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(buyerAddress1?.ToString()))
|
||||
{
|
||||
<tr>
|
||||
<th>Address 1</th>
|
||||
<td>@buyerAddress1</td>
|
||||
</tr>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(buyerAddress2?.ToString()))
|
||||
{
|
||||
<tr>
|
||||
<th>Address 2</th>
|
||||
<td>@buyerAddress2</td>
|
||||
</tr>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(buyerCity?.ToString()))
|
||||
{
|
||||
<tr>
|
||||
<th>City</th>
|
||||
<td>@buyerCity</td>
|
||||
</tr>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(buyerState?.ToString()))
|
||||
{
|
||||
<tr>
|
||||
<th>State</th>
|
||||
<td>@buyerState</td>
|
||||
</tr>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(buyerCountry?.ToString()))
|
||||
{
|
||||
<tr>
|
||||
<th>Country</th>
|
||||
<td>@buyerCountry</td>
|
||||
</tr>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(buyerZip?.ToString()))
|
||||
{
|
||||
<tr>
|
||||
<th>Zip</th>
|
||||
<td>@buyerZip</td>
|
||||
</tr>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(buyerName?.ToString()))
|
||||
{
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<td>@buyerName</td>
|
||||
</tr>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(buyerEmail?.ToString()))
|
||||
{
|
||||
<tr>
|
||||
<th>Email</th>
|
||||
<td>
|
||||
<a href="mailto:@buyerEmail">@buyerEmail</a>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(buyerPhone?.ToString()))
|
||||
{
|
||||
<tr>
|
||||
<th>Phone</th>
|
||||
<td>@buyerPhone</td>
|
||||
</tr>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(buyerAddress1?.ToString()))
|
||||
{
|
||||
<tr>
|
||||
<th>Address 1</th>
|
||||
<td>@buyerAddress1</td>
|
||||
</tr>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(buyerAddress2?.ToString()))
|
||||
{
|
||||
<tr>
|
||||
<th>Address 2</th>
|
||||
<td>@buyerAddress2</td>
|
||||
</tr>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(buyerCity?.ToString()))
|
||||
{
|
||||
<tr>
|
||||
<th>City</th>
|
||||
<td>@buyerCity</td>
|
||||
</tr>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(buyerState?.ToString()))
|
||||
{
|
||||
<tr>
|
||||
<th>State</th>
|
||||
<td>@buyerState</td>
|
||||
</tr>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(buyerCountry?.ToString()))
|
||||
{
|
||||
<tr>
|
||||
<th>Country</th>
|
||||
<td>@buyerCountry</td>
|
||||
</tr>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(buyerZip?.ToString()))
|
||||
{
|
||||
<tr>
|
||||
<th>Zip</th>
|
||||
<td>@buyerZip</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@ -350,13 +327,13 @@
|
||||
|
||||
if (!string.IsNullOrEmpty(StoreId) && !string.IsNullOrEmpty(InvoiceId))
|
||||
{
|
||||
Dispatcher.Dispatch(new StoreState.FetchInvoice(StoreId, InvoiceId));
|
||||
if (Invoice == null)
|
||||
Dispatcher.Dispatch(new StoreState.FetchInvoice(StoreId, InvoiceId));
|
||||
if (PaymentMethods == null)
|
||||
Dispatcher.Dispatch(new StoreState.FetchInvoicePaymentMethods(StoreId, InvoiceId));
|
||||
}
|
||||
}
|
||||
|
||||
private string? _successMessage;
|
||||
private string? StoreId => AccountManager.CurrentStore?.Id;
|
||||
private AppUserStoreInfo? StoreInfo => StoreState.Value.StoreInfo;
|
||||
private InvoiceData? Invoice => !string.IsNullOrEmpty(InvoiceId) ? StoreState.Value.GetInvoice(InvoiceId!)?.Data : null;
|
||||
@ -376,14 +353,4 @@
|
||||
|
||||
private string GetTitle() => $"Invoice {Invoice?.Id}".Trim();
|
||||
private bool CanCheckout => Invoice is { Status: InvoiceStatus.New };
|
||||
|
||||
private async Task ToggleArchive()
|
||||
{
|
||||
await AccountManager.GetClient().ArchiveInvoice(StoreId, InvoiceId);
|
||||
_successMessage = "The invoice has been archived and will no longer appear in the invoice list by default";
|
||||
if (!string.IsNullOrEmpty(StoreId) && !string.IsNullOrEmpty(InvoiceId))
|
||||
{
|
||||
Dispatcher.Dispatch(new StoreState.FetchInvoice(StoreId, InvoiceId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
@attribute [Route(Routes.ChannelsPeers)]
|
||||
@using System.Text.RegularExpressions
|
||||
@using BTCPayApp.Core.Auth
|
||||
@using BTCPayApp.Core.Data
|
||||
@using BTCPayApp.UI.Components.Layout
|
||||
@ -12,6 +11,7 @@
|
||||
@using BTCPayServer.Lightning
|
||||
@using LNURL
|
||||
@using NBitcoin
|
||||
@using Newtonsoft.Json
|
||||
@using org.ldk.enums
|
||||
@using org.ldk.structs
|
||||
@using NodeInfo = BTCPayServer.Lightning.NodeInfo
|
||||
@ -45,76 +45,62 @@
|
||||
<h2>Peers</h2>
|
||||
<PeersList Peers="_peers" OnConnectPeer="ConnectPeer" OnDisconnectPeer="DisconnectPeer" OnUpdatePeer="UpdatePeer" UpdatingPeerId="@_updatingPeer" class="mb-4"/>
|
||||
|
||||
<h2>Connect Peer</h2>
|
||||
<ValidationEditContext @ref="_connectEditContext" Model="PeerModel" OnValidSubmit="ConnectToNewPeer" SuccessMessage="@_connectSuccessMessage" ErrorMessage="@_connectErrorMessage">
|
||||
<DataAnnotationsValidator/>
|
||||
<fieldset class="box">
|
||||
<div class="form-group">
|
||||
<label for="PeerUrl" class="form-label" data-required>Peer URL</label>
|
||||
<InputText @bind-Value="PeerModel.PeerUrl" id="PeerUrl" class="form-control"/>
|
||||
<ValidationMessage For="@(() => PeerModel.PeerUrl)"/>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-100" disabled="@(_connectEditContext!.Invalid || _connectingNewPeer)">
|
||||
@if (_connectingNewPeer)
|
||||
{
|
||||
<LoadingIndicator/>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>Connect Peer</span>
|
||||
}
|
||||
</button>
|
||||
</fieldset>
|
||||
</ValidationEditContext>
|
||||
|
||||
<h2>Channels</h2>
|
||||
<ChannelsList Channels="_channels" OnCloseChannel="CloseChannel" UpdatingChannelId="@_updatingChannel" class="mb-4"/>
|
||||
|
||||
@if (_peers?.Any() is true)
|
||||
@if (PeerModel != null)
|
||||
{
|
||||
<h2>Open Channel</h2>
|
||||
<ValidationEditContext @ref="_openChannelEditContext" Model="ChannelModel" OnValidSubmit="HandleOpenChannel" SuccessMessage="@_channelSuccessMessage" ErrorMessage="@_channelErrorMessage">
|
||||
<h2>Connect Peer</h2>
|
||||
<ValidationEditContext @ref="_connectEditContext" Model="PeerModel" OnValidSubmit="ConnectToNewPeer" SuccessMessage="@_connectSuccessMessage" ErrorMessage="@_connectErrorMessage">
|
||||
<DataAnnotationsValidator/>
|
||||
<fieldset class="box">
|
||||
@if (!ChannelModel.UseLNURL)
|
||||
{
|
||||
<div class="form-group">
|
||||
<div class="d-flex justify-content-between">
|
||||
<label for="PeerId" class="form-label" data-required>Peer</label>
|
||||
<button type="button" class="btn btn-link p-0" @onclick="() => { ChannelModel.UseLNURL = true; }">Use LNURL</button>
|
||||
</div>
|
||||
<InputSelect @bind-Value="ChannelModel.PeerId" id="PeerId" class="form-select">
|
||||
<option>Select peer</option>
|
||||
@foreach (var peer in _peers)
|
||||
{
|
||||
<option value="@peer.NodeId">
|
||||
@if (!string.IsNullOrEmpty(peer.Info?.Label))
|
||||
{
|
||||
@($"{peer.Info.Label} - ")
|
||||
}
|
||||
@peer.NodeId
|
||||
</option>
|
||||
}
|
||||
</InputSelect>
|
||||
<ValidationMessage For="@(() => ChannelModel.PeerId)"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="Amount" class="form-label" data-required>Amount</label>
|
||||
<InputAmount Id="Amount" @bind-Value="ChannelModel.Amount" Unit="@UnitMoney" Rate="@Rate" Currency="@Currency" OnToggleDisplayCurrency="ToggleDisplayCurrency"/>
|
||||
<ValidationMessage For="@(() => ChannelModel.Amount)"/>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="form-group">
|
||||
<div class="d-flex justify-content-between">
|
||||
<label for="PeerId" class="form-label" data-required>LNURL Channel Request</label>
|
||||
<button type="button" class="btn btn-link p-0" @onclick="() => { ChannelModel.UseLNURL = false; }">Select peer</button>
|
||||
</div>
|
||||
<InputText @bind-Value="ChannelModel.LNURL" id="LNURL" class="form-control"/>
|
||||
<ValidationMessage For="@(() => ChannelModel.LNURL)"/>
|
||||
</div>
|
||||
}
|
||||
<div class="form-group">
|
||||
<label for="PeerUrl" class="form-label" data-required>Peer URL</label>
|
||||
<InputText @bind-Value="PeerModel.PeerUrl" id="PeerUrl" class="form-control"/>
|
||||
<ValidationMessage For="@(() => PeerModel.PeerUrl)"/>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-100" disabled="@(_connectEditContext!.Invalid || _connectingNewPeer)">
|
||||
@if (_connectingNewPeer)
|
||||
{
|
||||
<LoadingIndicator/>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>Connect Peer</span>
|
||||
}
|
||||
</button>
|
||||
</fieldset>
|
||||
</ValidationEditContext>
|
||||
}
|
||||
|
||||
<h2>Channels</h2>
|
||||
<ChannelsList Channels="_channels" OnCloseChannel="CloseChannel" UpdatingPeerId="@_updatingPeer" class="mb-4"/>
|
||||
|
||||
@if (_peers?.Any() is true && ChannelModel != null)
|
||||
{
|
||||
<h2>Open Channel</h2>
|
||||
<ValidationEditContext @ref="_openChannelEditContext" Model="ChannelModel" OnValidSubmit="() => OpenChannel()" SuccessMessage="@_channelSuccessMessage" ErrorMessage="@_channelErrorMessage">
|
||||
<DataAnnotationsValidator/>
|
||||
<fieldset class="box">
|
||||
<div class="form-group">
|
||||
<label for="PeerId" class="form-label" data-required>Peer</label>
|
||||
<InputSelect @bind-Value="ChannelModel.PeerId" id="PeerId" class="form-select">
|
||||
<option>Select peer</option>
|
||||
@foreach (var peer in _peers)
|
||||
{
|
||||
<option value="@peer.NodeId">
|
||||
@peer.NodeId
|
||||
@if (!string.IsNullOrEmpty(peer.Info?.Label))
|
||||
{
|
||||
@($" - {peer.Info.Label}")
|
||||
}
|
||||
</option>
|
||||
}
|
||||
</InputSelect>
|
||||
<ValidationMessage For="@(() => ChannelModel.PeerId)"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="Amount" class="form-label" data-required>Amount</label>
|
||||
<InputAmount Id="Amount" @bind-Value="ChannelModel.Amount" Unit="@UnitMoney" Rate="@Rate" Currency="@Currency" OnToggleDisplayCurrency="ToggleDisplayCurrency" />
|
||||
<ValidationMessage For="@(() => ChannelModel.Amount)"/>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-100" disabled="@(_openChannelEditContext!.Invalid || _openingChannel)">
|
||||
@if (_openingChannel)
|
||||
{
|
||||
@ -131,7 +117,7 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<LightningNodeStateAlert NodeState="State.Value.LightningNodeState" ConnectionState="State.Value.ConnectionState"/>
|
||||
<LightningNodeStateAlert State="State.Value.LightningNodeState"/>
|
||||
}
|
||||
</Authorized>
|
||||
<NotAuthorized>
|
||||
@ -159,30 +145,23 @@
|
||||
private string? _channelSuccessMessage;
|
||||
private string? _channelErrorMessage;
|
||||
private ValidationEditContext? _openChannelEditContext;
|
||||
private OpenChannelModel ChannelModel { get; set; } = new();
|
||||
private OpenChannelModel? ChannelModel { get; set; } = new();
|
||||
|
||||
private bool _connectingNewPeer;
|
||||
private string? _connectSuccessMessage;
|
||||
private string? _connectErrorMessage;
|
||||
private ValidationEditContext? _connectEditContext;
|
||||
private readonly CancellationTokenSource _refreshCts = new ();
|
||||
private ConnectPeerModel PeerModel { get; set; } = new();
|
||||
private ConnectPeerModel? PeerModel { get; set; } = new();
|
||||
|
||||
private class OpenChannelModel
|
||||
{
|
||||
public bool UseLNURL { get; set; }
|
||||
[RequiredIf(nameof(UseLNURL), false)]
|
||||
public string? PeerId { get; set; }
|
||||
[RequiredIf(nameof(UseLNURL), false)]
|
||||
public decimal? Amount { get; set; }
|
||||
[RequiredIf(nameof(UseLNURL), true)]
|
||||
public string? LNURL { get; set; }
|
||||
[Required] public string? PeerId { get; set; }
|
||||
[Required] public decimal? Amount { get; set; }
|
||||
}
|
||||
|
||||
private class ConnectPeerModel
|
||||
{
|
||||
[Required]
|
||||
public string? PeerUrl { get; set; }
|
||||
[Required] public string? PeerUrl { get; set; }
|
||||
}
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
@ -192,25 +171,7 @@
|
||||
if (await LightningNodeManager.CanConfigureLightningNode())
|
||||
NavigationManager.NavigateTo(Routes.LightningSettings);
|
||||
else
|
||||
_ = RefreshData(_refreshCts.Token);
|
||||
}
|
||||
|
||||
protected override ValueTask DisposeAsyncCore(bool disposing)
|
||||
{
|
||||
base.DisposeAsyncCore(disposing);
|
||||
|
||||
_refreshCts.Cancel();
|
||||
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task RefreshData(CancellationToken token)
|
||||
{
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
await FetchData();
|
||||
await Task.Delay(TimeSpan.FromSeconds(15), token);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task FetchData()
|
||||
@ -232,19 +193,15 @@
|
||||
return chans.Select(channel =>
|
||||
{
|
||||
var details = channel.channelDetails;
|
||||
var counterpartyId = details?.get_counterparty().get_node_id();
|
||||
var counterpartyNodeId = counterpartyId is not null ? Convert.ToHexString(counterpartyId).ToLowerInvariant() : null;
|
||||
var channelId = details?.get_channel_id().get_a();
|
||||
var outbound = details?.get_outbound_capacity_msat();
|
||||
var inbound = details?.get_inbound_capacity_msat();
|
||||
var peer = peers.FirstOrDefault(p => string.Equals(p.NodeId, counterpartyNodeId, StringComparison.InvariantCultureIgnoreCase));
|
||||
|
||||
var chan = new LightningChannelModel
|
||||
{
|
||||
Id = channel.channel.Id,
|
||||
CounterpartyNodeId = counterpartyNodeId,
|
||||
CounterpartyLabel = peer?.Info?.Label,
|
||||
ChannelId = channelId,
|
||||
Connected = counterpartyId is not null && peer is not null,
|
||||
Connected = _peers?.Any(p => string.Equals(p.NodeId, details?.get_counterparty().get_node_id().ToString(), StringComparison.InvariantCultureIgnoreCase)) is true,
|
||||
Confirmations = details?.get_confirmations() is Option_u32Z.Option_u32Z_Some x1 ? x1.some : null,
|
||||
ConfirmationsRequired = details?.get_confirmations_required() is Option_u32Z.Option_u32Z_Some x2 ? x2.some : null,
|
||||
FundingTxHash = details != null ? Convert.ToHexString(details.get_funding_txo().get_txid()).ToLowerInvariant() : null,
|
||||
@ -267,21 +224,21 @@
|
||||
alternates.Remove(channel.channel.Id);
|
||||
chan.AlternateIds = alternates.ToArray();
|
||||
|
||||
if (details?.get_counterparty() is { } counterparty)
|
||||
{
|
||||
chan.CounterpartyNodeId = Convert.ToHexString(counterparty.get_node_id()).ToLowerInvariant();
|
||||
chan.CounterpartyLabel = peers.FirstOrDefault(p => p.NodeId == chan.CounterpartyNodeId && !string.IsNullOrEmpty(p.Info?.Label))?.Info?.Label;
|
||||
}
|
||||
|
||||
if (channel.channel.Archived)
|
||||
{
|
||||
chan.State = "Archived";
|
||||
}
|
||||
else if (details is null)
|
||||
{
|
||||
if (channel.channel.AdditionalData.TryGetValue("CloseReasonHuman", out var reasonHuman))
|
||||
{
|
||||
var state = reasonHuman.GetString()?.Replace("ClosureReason_", "") ?? "Closed";
|
||||
chan.State = Regex.Replace(state, "([A-Z])", " $1", RegexOptions.Compiled).Trim();
|
||||
}
|
||||
else
|
||||
{
|
||||
chan.State = "Unknown, probably closed";
|
||||
}
|
||||
chan.State = channel.channel.AdditionalData.TryGetValue("CloseReasonHuman", out var reasonHuman)
|
||||
? reasonHuman.GetString()
|
||||
: "Unknown but probably closed";
|
||||
}
|
||||
else if (details.get_channel_shutdown_state() is Option_ChannelShutdownStateZ.Option_ChannelShutdownStateZ_Some some && some.some != ChannelShutdownState.LDKChannelShutdownState_NotShuttingDown)
|
||||
{
|
||||
@ -297,7 +254,7 @@
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<LightningPeerModel>> GetPeers(Dictionary<string, PeerInfo> peerInfos)
|
||||
private async Task<IEnumerable<LightningPeerModel>?> GetPeers(Dictionary<string, PeerInfo> peerInfos)
|
||||
{
|
||||
if (Node == null) return [];
|
||||
var peers = await Node.GetPeers();
|
||||
@ -338,7 +295,7 @@
|
||||
|
||||
private async Task ConnectToNewPeer()
|
||||
{
|
||||
if (Node == null) return;
|
||||
if (Node == null || PeerModel == null) return;
|
||||
_connectSuccessMessage = _connectErrorMessage = _channelSuccessMessage = _channelErrorMessage = null;
|
||||
|
||||
try
|
||||
@ -350,8 +307,7 @@
|
||||
}
|
||||
|
||||
_connectingNewPeer = true;
|
||||
var result = await AsyncExtensions.RunInOtherThread(async () =>
|
||||
await Node.PeerHandler.ConnectAsync(nodeInfo, CancellationToken.None));
|
||||
var result = await Node.PeerHandler.ConnectAsync(nodeInfo, CancellationToken.None);
|
||||
if (result is not null)
|
||||
{
|
||||
_connectSuccessMessage = $"Connection to {PeerModel.PeerUrl} initiated";
|
||||
@ -369,16 +325,7 @@
|
||||
|
||||
_connectingNewPeer = false;
|
||||
if (string.IsNullOrEmpty(_connectErrorMessage))
|
||||
_ = RunAsyncWithDelay(FetchData);
|
||||
}
|
||||
|
||||
// run with delay to allow peer manager to process events
|
||||
private async Task RunAsyncWithDelay(Func<Task> t)
|
||||
{
|
||||
var token = _refreshCts.Token;
|
||||
await Task.Delay(TimeSpan.FromSeconds(1), token);
|
||||
await t();
|
||||
await InvokeAsync(StateHasChanged);
|
||||
await FetchData();
|
||||
}
|
||||
|
||||
private async Task ConnectPeer(LightningPeerModel peer)
|
||||
@ -390,8 +337,7 @@
|
||||
try
|
||||
{
|
||||
if (EndPointParser.TryParse(peer.Socket, 9735, out var endpoint))
|
||||
await AsyncExtensions.RunInOtherThread(async () =>
|
||||
await Node.PeerHandler.ConnectAsync(new PubKey(peer.NodeId), endpoint));
|
||||
await Node.PeerHandler.ConnectAsync(new PubKey(peer.NodeId), endpoint);
|
||||
else
|
||||
_connectErrorMessage = $"Could not resolve endpoint for socket {peer.Socket}";
|
||||
}
|
||||
@ -401,8 +347,7 @@
|
||||
}
|
||||
|
||||
_updatingPeer = null;
|
||||
|
||||
_ = RunAsyncWithDelay(FetchData);
|
||||
await FetchData();
|
||||
}
|
||||
|
||||
private async Task DisconnectPeer(LightningPeerModel peer)
|
||||
@ -413,8 +358,7 @@
|
||||
|
||||
try
|
||||
{
|
||||
await AsyncExtensions.RunInOtherThread(async () =>
|
||||
await Node.PeerHandler.DisconnectAsync(new PubKey(peer.NodeId)));
|
||||
await Node.PeerHandler.DisconnectAsync(new PubKey(peer.NodeId));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@ -422,8 +366,7 @@
|
||||
}
|
||||
|
||||
_updatingPeer = null;
|
||||
|
||||
_ = RunAsyncWithDelay(FetchData);
|
||||
await FetchData();
|
||||
}
|
||||
|
||||
private async Task UpdatePeer(LightningPeerModel peer)
|
||||
@ -436,17 +379,9 @@
|
||||
await FetchData();
|
||||
}
|
||||
|
||||
private async Task HandleOpenChannel()
|
||||
{
|
||||
if (ChannelModel.UseLNURL)
|
||||
await ParseChannelRequest();
|
||||
else
|
||||
await OpenChannel();
|
||||
}
|
||||
|
||||
private async Task OpenChannel()
|
||||
{
|
||||
if (Loading || Node == null || ChannelModel.Amount is null || string.IsNullOrEmpty(ChannelModel.PeerId)) return;
|
||||
if (Loading || Node == null || ChannelModel?.Amount is null || string.IsNullOrEmpty(ChannelModel.PeerId)) return;
|
||||
|
||||
_connectSuccessMessage = _connectErrorMessage = _channelSuccessMessage = _channelErrorMessage = null;
|
||||
_openingChannel = true;
|
||||
@ -454,14 +389,14 @@
|
||||
{
|
||||
var amount = new Money(ChannelModel.Amount.Value, MoneyUnit.Satoshi);
|
||||
var result = await Node.OpenChannel(amount, new PubKey(ChannelModel.PeerId));
|
||||
if (result is Result_ChannelIdAPIErrorZ.Result_ChannelIdAPIErrorZ_OK)
|
||||
if (result is Result_ChannelIdAPIErrorZ.Result_ChannelIdAPIErrorZ_OK ok)
|
||||
{
|
||||
_channelSuccessMessage = "Channel open initiated";
|
||||
_channelSuccessMessage = $"Channel creation started with id {Convert.ToHexString(ok.res.get_a())}";
|
||||
ChannelModel = new OpenChannelModel();
|
||||
}
|
||||
else if (result is Result_ChannelIdAPIErrorZ.Result_ChannelIdAPIErrorZ_Err err)
|
||||
{
|
||||
_channelErrorMessage = err.err.GetError();
|
||||
_channelErrorMessage = $"Error: {err.err.GetError()}";
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
@ -471,26 +406,25 @@
|
||||
|
||||
_openingChannel = false;
|
||||
if (string.IsNullOrEmpty(_channelErrorMessage))
|
||||
_ = RunAsyncWithDelay(FetchData);
|
||||
await FetchData();
|
||||
}
|
||||
|
||||
private async Task ParseChannelRequest()
|
||||
private async Task ParseChannelRequest(string lnurl, string name, bool trusted)
|
||||
{
|
||||
if (Loading || Node == null || string.IsNullOrEmpty(ChannelModel.LNURL)) return;
|
||||
if (Loading || Node == null || ChannelModel?.Amount is null || string.IsNullOrEmpty(ChannelModel.PeerId)) return;
|
||||
_connectSuccessMessage = _connectErrorMessage = _channelSuccessMessage = _channelErrorMessage = null;
|
||||
_openChannelEditContext!.MessageStore!.Clear();
|
||||
|
||||
try
|
||||
{
|
||||
var uri = LNURL.Parse(ChannelModel.LNURL, out var tag);
|
||||
var uri = LNURL.Parse(lnurl, out var tag);
|
||||
var http = HttpClientFactory.CreateClient();
|
||||
var channelRequest = (LNURLChannelRequest) await LNURL.FetchInformation(uri, tag, http, new CancellationTokenSource(TimeSpan.FromSeconds(5)).Token);
|
||||
if (channelRequest is null)
|
||||
{
|
||||
_openChannelEditContext!.MessageStore!.Add(() => ChannelModel.LNURL, "The channel request is invalid");
|
||||
_openChannelEditContext!.NotifyValidationStateChanged();
|
||||
_channelErrorMessage = "The channel request is invalid";
|
||||
return;
|
||||
}
|
||||
|
||||
if (!EndPointParser.TryParse($"{channelRequest.Uri.Host}:{channelRequest.Uri.Port}", 9735, out var endpoint))
|
||||
{
|
||||
_channelErrorMessage = "The channel request provided an invalid endpoint for the peer";
|
||||
@ -498,22 +432,21 @@
|
||||
}
|
||||
|
||||
_openingChannel = true;
|
||||
await Node.Peer(channelRequest.Uri.NodeId, new PeerInfo { Endpoint = endpoint, Trusted = true });
|
||||
await Node.Peer(channelRequest.Uri.NodeId, new PeerInfo { Endpoint = endpoint, Label = name, Trusted = trusted });
|
||||
await ConnectPeer(new LightningPeerModel { NodeId = channelRequest.Uri.NodeId.ToString(), Socket = endpoint.ToEndpointString() });
|
||||
await channelRequest.SendRequest(Node.NodeId, true, http, new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token);
|
||||
_channelSuccessMessage = "Successfully initiated channel creation. Please wait for the peer to execute.";
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_openChannelEditContext!.MessageStore!.Add(() => ChannelModel.LNURL, e.Message);
|
||||
_openChannelEditContext!.NotifyValidationStateChanged();
|
||||
_channelErrorMessage = e.Message;
|
||||
}
|
||||
|
||||
_openingChannel = false;
|
||||
if (_channelSuccessMessage is null && _channelErrorMessage is null)
|
||||
_channelErrorMessage = "Error opening channel";
|
||||
else
|
||||
_ = RunAsyncWithDelay(FetchData);
|
||||
await FetchData();
|
||||
}
|
||||
|
||||
private async Task CloseChannel(LightningChannelModel channel)
|
||||
@ -542,12 +475,12 @@
|
||||
var result = await Node.CloseChannel(channelId, counterparty, force);
|
||||
if (result is Result_NoneAPIErrorZ.Result_NoneAPIErrorZ_OK)
|
||||
{
|
||||
_channelSuccessMessage = $"{(force ? "Force-closing" : "Closing")} channel with {counterparty}.";
|
||||
_channelSuccessMessage = $"{(force ? "Force-closing" : "Closing")} channel {channelId} with {counterparty}.";
|
||||
ChannelModel = new OpenChannelModel();
|
||||
}
|
||||
else if (result is Result_NoneAPIErrorZ.Result_NoneAPIErrorZ_Err err)
|
||||
{
|
||||
_channelErrorMessage = $"{(force ? "Force-closing" : "Closing")} channel with {counterparty} failed: {err.err.GetError()}";
|
||||
_channelErrorMessage = $"{(force ? "Force-closing" : "Closing")} channel {channelId} with {counterparty} failed: {err.err.GetError()}";
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
@ -556,7 +489,7 @@
|
||||
}
|
||||
|
||||
_updatingChannel = null;
|
||||
_ = RunAsyncWithDelay(FetchData);
|
||||
await FetchData();
|
||||
}
|
||||
|
||||
private void ToggleDisplayCurrency()
|
||||
|
||||
@ -68,7 +68,7 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<LightningNodeStateAlert NodeState="State.Value.LightningNodeState" ConnectionState="State.Value.ConnectionState" />
|
||||
<LightningNodeStateAlert State="State.Value.LightningNodeState"/>
|
||||
}
|
||||
</Authorized>
|
||||
<NotAuthorized>
|
||||
|
||||
@ -60,7 +60,7 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<LightningNodeStateAlert NodeState="State.Value.LightningNodeState" ConnectionState="State.Value.ConnectionState"/>
|
||||
<LightningNodeStateAlert State="State.Value.LightningNodeState"/>
|
||||
}
|
||||
</Authorized>
|
||||
<NotAuthorized>
|
||||
|
||||
@ -5,7 +5,6 @@
|
||||
@using Microsoft.Extensions.Logging
|
||||
@using BTCPayApp.UI.Components.Layout
|
||||
@using BTCPayApp.Core.Auth
|
||||
@using BTCPayApp.Core.BTCPayServer
|
||||
@using BTCPayApp.Core.LDK
|
||||
@using BTCPayApp.Core.Wallet
|
||||
@using BTCPayServer.Client.Models
|
||||
@ -31,11 +30,7 @@
|
||||
<section class="container">
|
||||
<AuthorizeView Policy="@AppPolicies.CanModifySettings">
|
||||
<Authorized>
|
||||
@if (State.Value.LightningNodeState is not LightningNodeState.Loaded)
|
||||
{
|
||||
<LightningNodeStateAlert NodeState="State.Value.LightningNodeState" ConnectionState="State.Value.ConnectionState" />
|
||||
}
|
||||
@if (State.Value.LightningNodeState is LightningNodeState.NotConfigured && AppSettings.AllowWalletGeneration)
|
||||
@if (State.Value.LightningNodeState is LightningNodeState.NotConfigured)
|
||||
{
|
||||
<button class="btn btn-primary" @onclick="ConfigureLNWallet">Configure Lightning Wallet</button>
|
||||
}
|
||||
@ -43,19 +38,20 @@
|
||||
{
|
||||
<h2 class="d-flex flex-wrap align-items-center gap-2">
|
||||
<span>@(_config?.Alias ?? "Lightning Node")</span>
|
||||
@if (IsStorePaymentMethod is true)
|
||||
@if (!string.IsNullOrEmpty(_storePaymentMethodIdentifier)
|
||||
// && _storePaymentMethodIdentifier == ConnectionString
|
||||
)
|
||||
{
|
||||
<span class="badge bg-info">Lightning Payment Method</span>
|
||||
}
|
||||
</h2>
|
||||
<div class="box">
|
||||
@if (LightningNodeManager.IsActive)
|
||||
<div class="box mb-5">
|
||||
@if (Node is not null)
|
||||
{
|
||||
<div class="form-floating">
|
||||
<TruncateCenter Text="@Node.NodeId.ToString()" Padding="15" Copy="true" Elastic="true" class="form-control-plaintext"/>
|
||||
<label>Node ID</label>
|
||||
</div>
|
||||
<code class="text-wrap">@Node.NodeId.ToString()</code>
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrEmpty(_config?.LightningDerivationPath))
|
||||
@ -66,20 +62,25 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrEmpty(ConnectionString))
|
||||
{
|
||||
<div class="form-floating">
|
||||
<TruncateCenter Text="@ConnectionString" Padding="15" Copy="true" Elastic="true" class="form-control-plaintext"/>
|
||||
<label>Connection String</label>
|
||||
</div>
|
||||
@if (IsStorePaymentMethod is false)
|
||||
{
|
||||
<button class="btn btn-primary my-3" @onclick="SetStorePaymentMethod">Set as lightning payment method for store</button>
|
||||
}
|
||||
}
|
||||
@* @if (!string.IsNullOrEmpty(ConnectionString)) *@
|
||||
@* { *@
|
||||
@* <div class="form-floating"> *@
|
||||
@* <TruncateCenter Text="@ConnectionString" Padding="15" Copy="true" Elastic="true" class="form-control-plaintext"/> *@
|
||||
@* <label>Connection String</label> *@
|
||||
@* </div> *@
|
||||
@* *@
|
||||
@* @if (!string.IsNullOrEmpty(StoreId) && _storePaymentMethodIdentifier != ConnectionString) *@
|
||||
@* { *@
|
||||
@* <button class="btn btn-primary my-3" @onclick="SetStorePaymentMethod">Set as lightning payment method for store</button> *@
|
||||
@* } *@
|
||||
@* } *@
|
||||
</div>
|
||||
}
|
||||
@if (State.Value.LightningNodeState is LightningNodeState.Init or LightningNodeState.Loaded or LightningNodeState.Stopped or LightningNodeState.Error && State.Value.ConnectionState != BTCPayConnectionState.ConnectedAsSecondary)
|
||||
else
|
||||
{
|
||||
<LightningNodeStateAlert State="State.Value.LightningNodeState" />
|
||||
}
|
||||
@if (State.Value.LightningNodeState is LightningNodeState.Init or LightningNodeState.Loaded or LightningNodeState.Stopped or LightningNodeState.Error)
|
||||
{
|
||||
<h2>Lightning Control</h2>
|
||||
<div class="box mb-2 d-grid d-sm-flex flex-wrap gap-3 buttons">
|
||||
@ -111,7 +112,7 @@
|
||||
@if (Node is not null)
|
||||
{
|
||||
<h2>Lightning Settings</h2>
|
||||
<ValidationEditContext @ref="_validationEditContext" Model="Model" OnValidSubmit="UpdateConfig" SuccessMessage="@SuccessMessage" ErrorMessage="@ErrorMessage">
|
||||
<ValidationEditContext @ref="_validationEditContext" Model="Model" OnValidSubmit="UpdateConfig" SuccessMessage="@SuccessMessage" ErrorMessage="@ErrorMessage">
|
||||
<DataAnnotationsValidator/>
|
||||
<fieldset class="box">
|
||||
@if (_jitOptions?.Any() is true)
|
||||
@ -150,14 +151,12 @@
|
||||
@code {
|
||||
private LightningConfig? _config;
|
||||
private LDKNode? Node => LightningNodeManager.Node;
|
||||
private APIKey? _apiKey;
|
||||
private string? StoreId => AccountManager.CurrentStore?.Id;
|
||||
private static string PaymentMethodId => LightningNodeManager.PaymentMethodId;
|
||||
private string? _storePaymentMethodIdentifier;
|
||||
private string? ConnectionString => AccountManager.UserInfo?.UserId is {} userId && _apiKey is not null ? _apiKey.ConnectionString(userId) : null;
|
||||
private bool? IsStorePaymentMethod => !string.IsNullOrEmpty(ConnectionString) && string.Equals(_storePaymentMethodIdentifier, ConnectionString);
|
||||
// We need a ui/component for the api keys now.
|
||||
// private string? ConnectionString => LightningNodeManager.ConnectionString;
|
||||
private string[]? _jitOptions;
|
||||
|
||||
private SettingsModel Model { get; set; } = new();
|
||||
private string? SuccessMessage { get; set; }
|
||||
private string? ErrorMessage { get; set; }
|
||||
@ -181,9 +180,6 @@
|
||||
|
||||
if (!string.IsNullOrEmpty(StoreId))
|
||||
await GetStorePaymentMethod();
|
||||
|
||||
if (!string.IsNullOrEmpty(StoreId) && Node?.ApiKeyManager is not null)
|
||||
_apiKey = await Node.ApiKeyManager.GetKeyForStore(StoreId, APIKeyPermission.Write);
|
||||
}
|
||||
|
||||
protected override ValueTask DisposeAsyncCore(bool disposing)
|
||||
@ -204,7 +200,7 @@
|
||||
{
|
||||
await Node.UpdateConfig(config =>
|
||||
{
|
||||
config.JITLSP = string.IsNullOrEmpty(Model.JitLsp) ? null : Model.JitLsp;
|
||||
config.JITLSP = Model.JitLsp;
|
||||
config.RapidGossipSyncUrl = string.IsNullOrEmpty(Model.RgsUrl) ? null : Uri.TryCreate(Model.RgsUrl, UriKind.Absolute, out var uri) ? uri: null;
|
||||
return Task.FromResult((config, true));
|
||||
});
|
||||
@ -238,29 +234,29 @@
|
||||
Logger.LogDebug(ex, "Payment method {PaymentMethodId} unset for store {StoreId}", PaymentMethodId, StoreId);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SetStorePaymentMethod()
|
||||
{
|
||||
if (string.IsNullOrEmpty(StoreId) || string.IsNullOrEmpty(ConnectionString)) return;
|
||||
SuccessMessage = ErrorMessage = null;
|
||||
try
|
||||
{
|
||||
var pm = await AccountManager.GetClient().UpdateStorePaymentMethod(StoreId, PaymentMethodId, new UpdatePaymentMethodRequest
|
||||
{
|
||||
Enabled = true,
|
||||
Config = ConnectionString
|
||||
});
|
||||
_storePaymentMethodIdentifier = GetConnectionString(pm);
|
||||
SuccessMessage = "Store payment method set.";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ErrorMessage = $"Error setting store payment method: {ex.Message}";
|
||||
Logger.LogError(ex, "Error setting payment method {PaymentMethodId} for store {StoreId}", PaymentMethodId, StoreId);
|
||||
}
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
//
|
||||
// private async Task SetStorePaymentMethod()
|
||||
// {
|
||||
// if (string.IsNullOrEmpty(StoreId) || string.IsNullOrEmpty(ConnectionString)) return;
|
||||
// _successMessage = _errorMessage = null;
|
||||
// try
|
||||
// {
|
||||
// var pm = await AccountManager.GetClient().UpdateStorePaymentMethod(StoreId, PaymentMethodId, new UpdatePaymentMethodRequest
|
||||
// {
|
||||
// Enabled = true,
|
||||
// Config = ConnectionString
|
||||
// });
|
||||
// _storePaymentMethodIdentifier = GetConnectionString(pm);
|
||||
// _successMessage = "Store payment method set.";
|
||||
// }
|
||||
// catch (Exception ex)
|
||||
// {
|
||||
// _errorMessage = $"Error setting store payment method: {ex.Message}";
|
||||
// Logger.LogError(ex, "Error setting payment method {PaymentMethodId} for store {StoreId}", PaymentMethodId, StoreId);
|
||||
// }
|
||||
//
|
||||
// await InvokeAsync(StateHasChanged);
|
||||
// }
|
||||
|
||||
private async Task ConfigureLNWallet()
|
||||
{
|
||||
|
||||
@ -82,7 +82,7 @@ else
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task CreateInvoice(Core.Models.CreatePosInvoiceRequest req)
|
||||
private async Task CreateInvoice(CreatePosInvoiceRequest req)
|
||||
{
|
||||
_errorMessage = null;
|
||||
req.AppId = AppId;
|
||||
|
||||
@ -1,28 +1,25 @@
|
||||
@attribute [Route(Routes.AppLogs)]
|
||||
@using System.Text
|
||||
@using BTCPayApp.Core.Contracts
|
||||
@using BTCPayApp.Core.Services
|
||||
@using BTCPayApp.UI.Components.Layout
|
||||
@using Serilog.Events
|
||||
@inject ConfigProvider ConfigProvider
|
||||
@inject LoggingService LogService
|
||||
@inject IJSRuntime JS
|
||||
@inject IEmailService EmailService
|
||||
|
||||
|
||||
<PageTitle>Application Logs</PageTitle>
|
||||
|
||||
<SectionContent SectionId="_Layout.Top">
|
||||
<Titlebar Back>
|
||||
<h1>Logs</h1>
|
||||
<SectionContent SectionId="Titlebar.End">
|
||||
<InputSelect @bind-Value="_logLevel" @bind-Value:after="LevelChanged" id="LogLevel" class="form-select form-select-sm px-2 py-1 w-auto" style="padding-right: var(--btcpay-space-l) !important;background-position-x:90%;">
|
||||
@foreach (var lvl in LoggingService.Levels)
|
||||
<InputSelect @bind-Value="LogService.LogLevel" @bind-Value:after="() => PersistLogLevel(LogService.LogLevel)" id="LogLevel" class="form-select form-select-sm px-2 py-1 w-auto" style="padding-right: var(--btcpay-space-l) !important;background-position-x:90%;">
|
||||
@foreach (var lvl in LogLevels)
|
||||
{
|
||||
<option value="@lvl" selected="@(_logLevel == lvl)">@(lvl == LogEventLevel.Information ? "Info" : lvl)</option>
|
||||
}
|
||||
</InputSelect>
|
||||
<InputSelect @bind-Value="_logCount" @bind-Value:after="CountChanged" id="LogCount" class="form-select form-select-sm px-2 py-1 w-auto" style="padding-right: var(--btcpay-space-l) !important;background-position-x:90%;">
|
||||
@foreach (var count in Counts)
|
||||
{
|
||||
<option value="@count" selected="@(_logCount == count)">@count</option>
|
||||
<option value="@lvl" selected="@(LogService.LogLevel == lvl)">@lvl</option>
|
||||
}
|
||||
</InputSelect>
|
||||
</SectionContent>
|
||||
@ -42,10 +39,6 @@
|
||||
}
|
||||
@if (HasLogs)
|
||||
{
|
||||
<div id="CtaContainer" class="container d-flex flex-column gap-3">
|
||||
<button type="button" class="btn btn-secondary" data-clipboard-target="#logs">Copy logs</button>
|
||||
<button type="button" class="btn btn-primary" @onclick="SendLogs">Send via email</button>
|
||||
</div>
|
||||
<pre><code id="logs">@_logContent</code></pre>
|
||||
}
|
||||
else
|
||||
@ -54,15 +47,20 @@
|
||||
<p class="text-muted my-0">There are no logs, yet.</p>
|
||||
</div>
|
||||
}
|
||||
@if (HasLogs)
|
||||
{
|
||||
<div id="CtaContainer" class="container d-flex align-items-center justify-content-between">
|
||||
<button type="button" class="btn btn-primary w-100" @onclick="SaveLogs">Send logs</button>
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
|
||||
@code {
|
||||
private bool _isLoading;
|
||||
private string? _logContent;
|
||||
private int _logCount = Counts.First();
|
||||
private string? _errorMessage;
|
||||
private LogEventLevel _logLevel = LogEventLevel.Information;
|
||||
private static readonly int[] Counts = [50, 100, 200, 500];
|
||||
|
||||
private static LogEventLevel[] LogLevels => [LogEventLevel.Error, LogEventLevel.Warning, LogEventLevel.Information, LogEventLevel.Debug, LogEventLevel.Verbose];
|
||||
private bool HasLogs => !string.IsNullOrEmpty(_logContent);
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
@ -70,8 +68,7 @@
|
||||
try
|
||||
{
|
||||
_isLoading = true;
|
||||
_logLevel = await ConfigProvider.Get<LogEventLevel?>("logLevel") ?? LogEventLevel.Information;
|
||||
await RefreshLogContent();
|
||||
_logContent = await LogService.GetLatestLogAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@ -83,24 +80,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LevelChanged()
|
||||
private async Task PersistLogLevel(LogEventLevel logLevel)
|
||||
{
|
||||
await ConfigProvider.Set("logLevel", _logLevel, false);
|
||||
await RefreshLogContent();
|
||||
await ConfigProvider.Set("logLevel", logLevel, false);
|
||||
}
|
||||
|
||||
private async Task CountChanged()
|
||||
private async Task SaveLogs()
|
||||
{
|
||||
await RefreshLogContent();
|
||||
}
|
||||
|
||||
private async Task RefreshLogContent()
|
||||
{
|
||||
_logContent = await LogService.GetLatestLogAsync(_logLevel, _logCount);
|
||||
}
|
||||
|
||||
private async Task SendLogs()
|
||||
{
|
||||
await EmailService.SendAsync("App Log File", _logContent!, "app@btcpayserver.org");
|
||||
//var bytes = Encoding.UTF8.GetBytes(_logContent!);
|
||||
//await JS.InvokeAsync<object>("Interop.saveAsFile", "btcpayapp.log", Convert.ToBase64String(bytes));
|
||||
var logFilePath = await LogService.GetAppLogFilePath();
|
||||
await EmailService.SendAsync("App Log File", _logContent!);
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,7 +3,6 @@
|
||||
@using BTCPayApp.Core.Auth
|
||||
@using BTCPayApp.Core.BTCPayServer
|
||||
@using BTCPayApp.Core.Contracts
|
||||
@using BTCPayApp.Core.Helpers
|
||||
@using BTCPayApp.Core.Models
|
||||
@using BTCPayApp.Core.Wallet
|
||||
@using BTCPayApp.UI.Features
|
||||
@ -215,30 +214,27 @@
|
||||
</div>
|
||||
</AuthorizeView>
|
||||
|
||||
@if (State.Value.ConnectionState == BTCPayConnectionState.ConnectedAsPrimary)
|
||||
{
|
||||
<AuthorizeView Policy="@AppPolicies.CanModifySettings">
|
||||
<h2>Bitcoin</h2>
|
||||
<div class="box">
|
||||
<ul class="list-group list-group-flush list-group-links">
|
||||
<li class="list-group-item">
|
||||
<a href="@Routes.WalletSettings">
|
||||
<Icon Symbol="wallet-wallet"/>
|
||||
<span>Onchain Wallet</span>
|
||||
<Icon Symbol="caret-right"/>
|
||||
</a>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<a href="@Routes.LightningSettings">
|
||||
<Icon Symbol="lightning-node"/>
|
||||
<span>Lightning Node</span>
|
||||
<Icon Symbol="caret-right"/>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</AuthorizeView>
|
||||
}
|
||||
<AuthorizeView Policy="@AppPolicies.CanModifySettings">
|
||||
<h2>Bitcoin</h2>
|
||||
<div class="box">
|
||||
<ul class="list-group list-group-flush list-group-links">
|
||||
<li class="list-group-item">
|
||||
<a href="@Routes.WalletSettings">
|
||||
<Icon Symbol="wallet-wallet"/>
|
||||
<span>Onchain Wallet</span>
|
||||
<Icon Symbol="caret-right"/>
|
||||
</a>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<a href="@Routes.LightningSettings">
|
||||
<Icon Symbol="lightning-node"/>
|
||||
<span>Lightning Node</span>
|
||||
<Icon Symbol="caret-right"/>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</AuthorizeView>
|
||||
}
|
||||
|
||||
<AuthorizeView Policy="@AppPolicies.CanModifySettings">
|
||||
@ -309,10 +305,12 @@
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
@if (InstanceInfo != null)
|
||||
{
|
||||
</li>
|
||||
@if (InstanceInfo != null)
|
||||
{
|
||||
<li class="list-group-item">
|
||||
<div class="justify-content-start">
|
||||
<span class="ms-2 me-3 btcpay-status invisible"></span>
|
||||
<span class="m-2 me-3 btcpay-status btcpay-status--@(State.Value.ConnectionState switch { BTCPayConnectionState.ConnectedAsPrimary or BTCPayConnectionState.ConnectedAsSecondary or BTCPayConnectionState.ConnectedFinishedInitialSync => "enabled", BTCPayConnectionState.Disconnected => "disabled", _ => "pending" })"></span>
|
||||
<span>
|
||||
Instance: @InstanceInfo.BaseUrl
|
||||
@if (!string.IsNullOrEmpty(InstanceInfo.ServerName))
|
||||
@ -321,17 +319,8 @@
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
@if (_deviceId != 0)
|
||||
{
|
||||
<div class="justify-content-start">
|
||||
<span class="m-2 me-3 btcpay-status invisible"></span>
|
||||
<span>
|
||||
Device ID: @_deviceId
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
</li>
|
||||
</li>
|
||||
}
|
||||
@if (State.Value.ConnectionState == BTCPayConnectionState.ConnectedAsPrimary)
|
||||
{
|
||||
<li class="list-group-item">
|
||||
@ -390,22 +379,22 @@
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<h2>Logout</h2>
|
||||
<div class="box">
|
||||
@switch (State.Value.ConnectionState)
|
||||
{
|
||||
case BTCPayConnectionState.ConnectedAsPrimary:
|
||||
<p>This device is currently connected as primary device for communication with the BTCPay Server.</p>
|
||||
<p>Please note, that when you sign out of the account on this device, your Lightning node will go offline and you will not be able to receive any payments.</p>
|
||||
break;
|
||||
case BTCPayConnectionState.ConnectedAsSecondary:
|
||||
<p>This device is currently connected as additional device for communication with the BTCPay Server.</p>
|
||||
<p>Please ensure, that your primary device is still connected to the BTCPay Server, because otherwise you will not be able to receive any payments.</p>
|
||||
break;
|
||||
}
|
||||
<button class="btn btn-outline-danger w-100 mt-2" type="button" @onclick="Logout">Logout</button>
|
||||
</div>
|
||||
</AuthorizeView>
|
||||
<h2>Logout</h2>
|
||||
<div class="box">
|
||||
@switch (State.Value.ConnectionState)
|
||||
{
|
||||
case BTCPayConnectionState.ConnectedAsPrimary:
|
||||
<p>This device is currently connected as the primary device for communication with the BTCPay Server.</p>
|
||||
<p>Please note that when you sign out of the account on this device, your Lightning node will go offline, and you will not be able to receive any payments.</p>
|
||||
break;
|
||||
case BTCPayConnectionState.ConnectedAsSecondary:
|
||||
<p>This device is currently connected as an additional device for communication with the BTCPay Server.</p>
|
||||
<p>Please ensure that your primary device is still connected to the BTCPay Server, because otherwise you will not be able to receive any payments.</p>
|
||||
break;
|
||||
}
|
||||
<button class="btn btn-outline-danger w-100 mt-2" type="button" @onclick="Logout">Logout</button>
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
|
||||
@ -423,7 +412,6 @@
|
||||
private bool _sending;
|
||||
private string? _errorMessage;
|
||||
private int[]? _fees;
|
||||
private long _deviceId;
|
||||
|
||||
private class SettingsModel
|
||||
{
|
||||
@ -448,7 +436,6 @@
|
||||
|
||||
_account = AccountManager.Account;
|
||||
_config = await ConfigProvider.Get<BTCPayAppConfig>(BTCPayAppConfig.Key);
|
||||
_deviceId = await ConfigProvider.GetDeviceIdentifier();
|
||||
|
||||
Model.Theme = UiState.Value.SelectedTheme;
|
||||
ConnectionManager.ConnectionChanged += OnConnectionChanged;
|
||||
|
||||
@ -141,6 +141,7 @@
|
||||
// prevent duplicate submission due to quirk in QR reader lib
|
||||
if (code == _qrInput) return;
|
||||
_qrInput = code;
|
||||
Logger.LogInformation("Scanned QR code: {QrCode}", code);
|
||||
|
||||
Model.EncryptionKey = code;
|
||||
StateHasChanged();
|
||||
|
||||
@ -130,7 +130,7 @@
|
||||
{
|
||||
if (Stores?.Any() is not true)
|
||||
NavigationManager.NavigateTo(Routes.CreateStore);
|
||||
else if (Stores?.Count() == 1 && string.IsNullOrEmpty(_initialStoreId))
|
||||
else if (Stores?.Count() == 1)
|
||||
await SelectStore(Stores!.First().Id);
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user