From afc9d4a480ed3c3a8270da69ec048ac95d17e17c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20A=2EP?=
<53834183+Jossec101@users.noreply.github.com>
Date: Tue, 13 Oct 2026 11:01:27 +0200
Subject: [PATCH 1/6] Spark payment timeout in minutes, per node; rename the
max parts setting
A Spark swap's payment had a fixed 2-minute LND timeout. It is now SPARK_PAYMENT_TIMEOUT_MINUTES (10 by
default, 1 to 25 so LND gives up well before the 30-minute invoice expires), and each node can set its own
in its liquidity settings. MPP splitting stays the rebalances' one, whose setting is renamed
SEND_PAYMENT_MAX_PARTS as every SendPaymentV2 call uses it (REBALANCE_MAX_PARTS is still read).
---
docs/rebalance-algorithm.md | 2 +-
src/Data/Models/Node.cs | 6 +
src/Helpers/Constants.cs | 12 +-
...918_AddNodeSparkPaymentTimeout.Designer.cs | 2003 +++++++++++++++++
...261013085918_AddNodeSparkPaymentTimeout.cs | 28 +
.../ApplicationDbContextModelSnapshot.cs | 3 +
src/Pages/Nodes.razor | 18 +
src/Services/LightningService.cs | 2 +-
src/Services/Spark/SparkSettings.cs | 26 +
src/Services/Spark/SparkSwapService.cs | 3 +-
.../Services/Spark/SparkSettingsTests.cs | 15 +
.../Services/Spark/SparkSwapServiceTests.cs | 2 +-
12 files changed, 2110 insertions(+), 10 deletions(-)
create mode 100644 src/Migrations/20261013085918_AddNodeSparkPaymentTimeout.Designer.cs
create mode 100644 src/Migrations/20261013085918_AddNodeSparkPaymentTimeout.cs
diff --git a/docs/rebalance-algorithm.md b/docs/rebalance-algorithm.md
index 21715049..ec4f5a10 100644
--- a/docs/rebalance-algorithm.md
+++ b/docs/rebalance-algorithm.md
@@ -340,7 +340,7 @@ which one gets drained can differ between runs on an otherwise unchanged node.
| `REBALANCE_INITIAL_RETRY_DELAY_SECONDS` | `60` | First retry delay. |
| `REBALANCE_RETRY_BACKOFF_MULTIPLIER` | `2.0` | `delay = 60 · 2^(attempt−2)` ⇒ 60 s, 120 s. |
| `REBALANCE_AMOUNT_BACKOFF_RATIO` | `0.8` | Retries shrink the amount by this factor per attempt. |
-| `REBALANCE_MAX_PARTS` | `32` | Max MPP shards. |
+| `SEND_PAYMENT_MAX_PARTS` | `32` | Max MPP shards of a payment sent with SendPaymentV2: rebalances and Spark swaps. `REBALANCE_MAX_PARTS` is still read as a fallback. |
| `REBALANCE_DEFAULT_MAX_FEE_PCT` | `0.05` | Fallback cap when none is supplied (0.05 %). |
| `REBALANCE_DEFAULT_RETRY_MAX_FEE_PCT` | `0.05` | Fallback retry cap. |
| `REBALANCE_RECONCILE_TERMINAL_WINDOW_HOURS` | `24` | How far back `MonitorRebalancesJob` reconciles. |
diff --git a/src/Data/Models/Node.cs b/src/Data/Models/Node.cs
index 1614b5ba..9fd8a4ac 100644
--- a/src/Data/Models/Node.cs
+++ b/src/Data/Models/Node.cs
@@ -145,6 +145,12 @@ public class Node : Entity
///
public int? SparkWalletId { get; set; }
+ ///
+ /// How long LND keeps trying routes for this node's Spark swap payments, in minutes; null means
+ /// SPARK_PAYMENT_TIMEOUT_MINUTES
+ ///
+ public int? SparkPaymentTimeoutMinutes { get; set; }
+
#endregion Automatic Swap Out Configuration
#region Routing Engine
diff --git a/src/Helpers/Constants.cs b/src/Helpers/Constants.cs
index 91344a98..5673f35b 100644
--- a/src/Helpers/Constants.cs
+++ b/src/Helpers/Constants.cs
@@ -199,10 +199,11 @@ public class Constants
public static double REBALANCE_AMOUNT_BACKOFF_RATIO = 0.8;
///
- /// Maximum number of partial payments (MPP shards) LND may split a rebalance into. 1
- /// disables splitting; higher values let LND complete an amount across several routes.
+ /// Maximum number of partial payments (MPP shards) LND may split a payment sent with SendPaymentV2 into:
+ /// rebalances and Spark swap payments (SEND_PAYMENT_MAX_PARTS; REBALANCE_MAX_PARTS still read as a
+ /// fallback). 1 disables splitting; higher values let LND complete an amount across several routes.
///
- public static uint REBALANCE_MAX_PARTS = 32;
+ public static uint SEND_PAYMENT_MAX_PARTS = 32;
///
/// How far back the MonitorRebalancesJob sweeps for already-terminal-but-possibly-wrong
@@ -608,8 +609,9 @@ static Constants()
var rebProbeBackoff = Environment.GetEnvironmentVariable("REBALANCE_AMOUNT_BACKOFF_RATIO");
if (rebProbeBackoff != null) REBALANCE_AMOUNT_BACKOFF_RATIO = double.Parse(rebProbeBackoff, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture);
- var rebMaxParts = Environment.GetEnvironmentVariable("REBALANCE_MAX_PARTS");
- if (rebMaxParts != null) REBALANCE_MAX_PARTS = uint.Parse(rebMaxParts);
+ var sendPaymentMaxParts = Environment.GetEnvironmentVariable("SEND_PAYMENT_MAX_PARTS")
+ ?? Environment.GetEnvironmentVariable("REBALANCE_MAX_PARTS");
+ if (sendPaymentMaxParts != null) SEND_PAYMENT_MAX_PARTS = uint.Parse(sendPaymentMaxParts);
var rebReconcileWindow = Environment.GetEnvironmentVariable("REBALANCE_RECONCILE_TERMINAL_WINDOW_HOURS");
if (rebReconcileWindow != null) REBALANCE_RECONCILE_TERMINAL_WINDOW_HOURS = int.Parse(rebReconcileWindow);
diff --git a/src/Migrations/20261013085918_AddNodeSparkPaymentTimeout.Designer.cs b/src/Migrations/20261013085918_AddNodeSparkPaymentTimeout.Designer.cs
new file mode 100644
index 00000000..2e9d7ddd
--- /dev/null
+++ b/src/Migrations/20261013085918_AddNodeSparkPaymentTimeout.Designer.cs
@@ -0,0 +1,2003 @@
+//
+using System;
+using System.Collections.Generic;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using NodeGuard.Data;
+using NodeGuard.Helpers;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace NodeGuard.Migrations
+{
+ [DbContext(typeof(ApplicationDbContext))]
+ [Migration("20261013085918_AddNodeSparkPaymentTimeout")]
+ partial class AddNodeSparkPaymentTimeout
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "10.0.1")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("ApplicationUserNode", b =>
+ {
+ b.Property("NodesId")
+ .HasColumnType("integer");
+
+ b.Property("UsersId")
+ .HasColumnType("text");
+
+ b.HasKey("NodesId", "UsersId");
+
+ b.HasIndex("UsersId");
+
+ b.ToTable("ApplicationUserNode");
+ });
+
+ modelBuilder.Entity("ChannelOperationRequestFMUTXO", b =>
+ {
+ b.Property("ChannelOperationRequestsId")
+ .HasColumnType("integer");
+
+ b.Property("UtxosId")
+ .HasColumnType("integer");
+
+ b.HasKey("ChannelOperationRequestsId", "UtxosId");
+
+ b.HasIndex("UtxosId");
+
+ b.ToTable("ChannelOperationRequestFMUTXO");
+ });
+
+ modelBuilder.Entity("FMUTXOWalletWithdrawalRequest", b =>
+ {
+ b.Property("UTXOsId")
+ .HasColumnType("integer");
+
+ b.Property("WalletWithdrawalRequestsId")
+ .HasColumnType("integer");
+
+ b.HasKey("UTXOsId", "WalletWithdrawalRequestsId");
+
+ b.HasIndex("WalletWithdrawalRequestsId");
+
+ b.ToTable("FMUTXOWalletWithdrawalRequest");
+ });
+
+ modelBuilder.Entity("KeyWallet", b =>
+ {
+ b.Property("KeysId")
+ .HasColumnType("integer");
+
+ b.Property("WalletsId")
+ .HasColumnType("integer");
+
+ b.HasKey("KeysId", "WalletsId");
+
+ b.HasIndex("WalletsId");
+
+ b.ToTable("KeyWallet");
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("text");
+
+ b.Property("ConcurrencyStamp")
+ .IsConcurrencyToken()
+ .HasColumnType("text");
+
+ b.Property("Name")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("NormalizedName")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("NormalizedName")
+ .IsUnique()
+ .HasDatabaseName("RoleNameIndex");
+
+ b.ToTable("AspNetRoles", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("ClaimType")
+ .HasColumnType("text");
+
+ b.Property("ClaimValue")
+ .HasColumnType("text");
+
+ b.Property("RoleId")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("RoleId");
+
+ b.ToTable("AspNetRoleClaims", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("text");
+
+ b.Property("AccessFailedCount")
+ .HasColumnType("integer");
+
+ b.Property("ConcurrencyStamp")
+ .IsConcurrencyToken()
+ .HasColumnType("text");
+
+ b.Property("Discriminator")
+ .IsRequired()
+ .HasMaxLength(21)
+ .HasColumnType("character varying(21)");
+
+ b.Property("Email")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("EmailConfirmed")
+ .HasColumnType("boolean");
+
+ b.Property("LockoutEnabled")
+ .HasColumnType("boolean");
+
+ b.Property("LockoutEnd")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("NormalizedEmail")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("NormalizedUserName")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("PasswordHash")
+ .HasColumnType("text");
+
+ b.Property("PhoneNumber")
+ .HasColumnType("text");
+
+ b.Property("PhoneNumberConfirmed")
+ .HasColumnType("boolean");
+
+ b.Property("SecurityStamp")
+ .HasColumnType("text");
+
+ b.Property("TwoFactorEnabled")
+ .HasColumnType("boolean");
+
+ b.Property("UserName")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("NormalizedEmail")
+ .HasDatabaseName("EmailIndex");
+
+ b.HasIndex("NormalizedUserName")
+ .IsUnique()
+ .HasDatabaseName("UserNameIndex");
+
+ b.ToTable("AspNetUsers", (string)null);
+
+ b.HasDiscriminator().HasValue("IdentityUser");
+
+ b.UseTphMappingStrategy();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("ClaimType")
+ .HasColumnType("text");
+
+ b.Property("ClaimValue")
+ .HasColumnType("text");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AspNetUserClaims", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b =>
+ {
+ b.Property("LoginProvider")
+ .HasMaxLength(128)
+ .HasColumnType("character varying(128)");
+
+ b.Property("ProviderKey")
+ .HasMaxLength(128)
+ .HasColumnType("character varying(128)");
+
+ b.Property("ProviderDisplayName")
+ .HasColumnType("text");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("LoginProvider", "ProviderKey");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AspNetUserLogins", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b =>
+ {
+ b.Property("UserId")
+ .HasColumnType("text");
+
+ b.Property("RoleId")
+ .HasColumnType("text");
+
+ b.HasKey("UserId", "RoleId");
+
+ b.HasIndex("RoleId");
+
+ b.ToTable("AspNetUserRoles", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b =>
+ {
+ b.Property("UserId")
+ .HasColumnType("text");
+
+ b.Property("LoginProvider")
+ .HasMaxLength(128)
+ .HasColumnType("character varying(128)");
+
+ b.Property("Name")
+ .HasMaxLength(128)
+ .HasColumnType("character varying(128)");
+
+ b.Property("Value")
+ .HasColumnType("text");
+
+ b.HasKey("UserId", "LoginProvider", "Name");
+
+ b.ToTable("AspNetUserTokens", (string)null);
+ });
+
+ modelBuilder.Entity("NodeGuard.Data.Models.APIToken", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("CreationDatetime")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("CreatorId")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("ExpirationDate")
+ .HasColumnType("timestamp without time zone");
+
+ b.Property("IsBlocked")
+ .HasColumnType("boolean");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("TokenHash")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("UpdateDatetime")
+ .HasColumnType("timestamp with time zone");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CreatorId");
+
+ b.ToTable("ApiTokens");
+ });
+
+ modelBuilder.Entity("NodeGuard.Data.Models.AuditLog", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("ActionType")
+ .HasColumnType("integer");
+
+ b.Property("Details")
+ .HasColumnType("text");
+
+ b.Property("EventType")
+ .HasColumnType("integer");
+
+ b.Property("IpAddress")
+ .HasMaxLength(45)
+ .HasColumnType("character varying(45)");
+
+ b.Property("ObjectAffected")
+ .HasColumnType("integer");
+
+ b.Property("ObjectId")
+ .HasMaxLength(450)
+ .HasColumnType("character varying(450)");
+
+ b.Property("Timestamp")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UserId")
+ .HasMaxLength(450)
+ .HasColumnType("character varying(450)");
+
+ b.Property("Username")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.HasKey("Id");
+
+ b.ToTable("AuditLogs");
+ });
+
+ modelBuilder.Entity("NodeGuard.Data.Models.Channel", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("BtcCloseAddress")
+ .HasColumnType("text");
+
+ b.Property("ChanId")
+ .HasColumnType("numeric(20,0)");
+
+ b.Property("ClosedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("CreatedByNodeGuard")
+ .HasColumnType("boolean");
+
+ b.Property("CreationDatetime")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("DestinationNodeId")
+ .HasColumnType("integer");
+
+ b.Property("FundingTx")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("FundingTxOutputIndex")
+ .HasColumnType("bigint");
+
+ b.Property("IsAutoRebalanceEnabled")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("boolean")
+ .HasDefaultValue(false);
+
+ b.Property("IsAutomatedLiquidityEnabled")
+ .HasColumnType("boolean");
+
+ b.Property("IsDynamicFeeEnabled")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("boolean")
+ .HasDefaultValue(false);
+
+ b.Property("IsPrivate")
+ .HasColumnType("boolean");
+
+ b.Property("SatsAmount")
+ .HasColumnType("bigint");
+
+ b.Property("SourceNodeId")
+ .HasColumnType("integer");
+
+ b.Property("Status")
+ .HasColumnType("integer");
+
+ b.Property("UpdateDatetime")
+ .HasColumnType("timestamp with time zone");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DestinationNodeId");
+
+ b.HasIndex("SourceNodeId");
+
+ b.ToTable("Channels");
+ });
+
+ modelBuilder.Entity("NodeGuard.Data.Models.ChannelFeeState", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("ChannelId")
+ .HasColumnType("integer");
+
+ b.Property("CreationDatetime")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("LastAppliedInboundBaseMsat")
+ .HasColumnType("integer");
+
+ b.Property("LastAppliedInboundPpm")
+ .HasColumnType("integer");
+
+ b.Property("LastAppliedOutboundBaseFeeMsat")
+ .HasColumnType("bigint");
+
+ b.Property("LastAppliedOutboundPpm")
+ .HasColumnType("bigint");
+
+ b.Property("LastComputedTarget")
+ .HasColumnType("double precision");
+
+ b.Property("LastFeeUpdateAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("LastObservedRatio")
+ .HasColumnType("double precision");
+
+ b.Property("ManagedNodePubKey")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("UpdateDatetime")
+ .HasColumnType("timestamp with time zone");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ChannelId", "ManagedNodePubKey")
+ .IsUnique();
+
+ b.ToTable("ChannelFeeStates");
+ });
+
+ modelBuilder.Entity("NodeGuard.Data.Models.ChannelOperationRequest", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("AmountCryptoUnit")
+ .HasColumnType("integer");
+
+ b.Property("Changeless")
+ .HasColumnType("boolean");
+
+ b.Property("ChannelId")
+ .HasColumnType("integer");
+
+ b.Property("ClosingReason")
+ .HasColumnType("text");
+
+ b.Property("CreationDatetime")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Description")
+ .HasColumnType("text");
+
+ b.Property("DestNodeId")
+ .HasColumnType("integer");
+
+ b.Property("FeeRate")
+ .HasColumnType("numeric");
+
+ b.Property("InitialChannelBaseFeeMsat")
+ .HasColumnType("bigint");
+
+ b.Property("InitialChannelFeeRatePpm")
+ .HasColumnType("bigint");
+
+ b.Property("IsChannelPrivate")
+ .HasColumnType("boolean");
+
+ b.Property("MempoolRecommendedFeesType")
+ .HasColumnType("integer");
+
+ b.Property("RequestType")
+ .HasColumnType("integer");
+
+ b.Property("SatsAmount")
+ .HasColumnType("bigint");
+
+ b.Property("SourceNodeId")
+ .HasColumnType("integer");
+
+ b.Property("Status")
+ .HasColumnType("integer");
+
+ b.Property>("StatusLogs")
+ .HasColumnType("jsonb");
+
+ b.Property("TxId")
+ .HasColumnType("text");
+
+ b.Property("UpdateDatetime")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UserId")
+ .HasColumnType("text");
+
+ b.Property("WalletId")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ChannelId");
+
+ b.HasIndex("DestNodeId");
+
+ b.HasIndex("SourceNodeId");
+
+ b.HasIndex("UserId");
+
+ b.HasIndex("WalletId");
+
+ b.ToTable("ChannelOperationRequests");
+ });
+
+ modelBuilder.Entity("NodeGuard.Data.Models.ChannelOperationRequestPSBT", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("ChannelOperationRequestId")
+ .HasColumnType("integer");
+
+ b.Property("CreationDatetime")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("IsFinalisedPSBT")
+ .HasColumnType("boolean");
+
+ b.Property("IsInternalWalletPSBT")
+ .HasColumnType("boolean");
+
+ b.Property("IsTemplatePSBT")
+ .HasColumnType("boolean");
+
+ b.Property("PSBT")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("UpdateDatetime")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UserSignerId")
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ChannelOperationRequestId");
+
+ b.HasIndex("UserSignerId");
+
+ b.HasIndex(new[] { "ChannelOperationRequestId" }, "IX_ChannelOperationRequestPSBTs_Template")
+ .IsUnique()
+ .HasFilter("\"IsTemplatePSBT\"");
+
+ b.ToTable("ChannelOperationRequestPSBTs");
+ });
+
+ modelBuilder.Entity("NodeGuard.Data.Models.ChannelRoutingState", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("AgeBlocks")
+ .HasColumnType("bigint");
+
+ b.Property("ChanIdLnd")
+ .HasColumnType("numeric(20,0)");
+
+ b.Property("ChannelId")
+ .HasColumnType("integer");
+
+ b.Property("ConsecutiveCategoryCyclesInNewState")
+ .HasColumnType("bigint");
+
+ b.Property("CreationDatetime")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("EmaLocalRatio")
+ .HasColumnType("double precision");
+
+ b.Property("FundingBlockHeight")
+ .HasColumnType("bigint");
+
+ b.Property("LastCategorizedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("LastEvaluatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("LastKnownLifetime")
+ .HasColumnType("bigint");
+
+ b.Property("LastKnownNumUpdates")
+ .HasColumnType("bigint");
+
+ b.Property("LastKnownUptime")
+ .HasColumnType("bigint");
+
+ b.Property("ManagedNodePubKey")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("NetFlowRatio")
+ .HasColumnType("double precision");
+
+ b.Property("PeerFlowCategory")
+ .HasColumnType("integer");
+
+ b.Property("PeerInitiated")
+ .HasColumnType("boolean");
+
+ b.Property("PendingCategory")
+ .HasColumnType("integer");
+
+ b.Property("PullMsatWindow")
+ .HasColumnType("bigint");
+
+ b.Property("PushMsatWindow")
+ .HasColumnType("bigint");
+
+ b.Property("TargetLocalRatio")
+ .HasColumnType("double precision");
+
+ b.Property("UpdateDatetime")
+ .HasColumnType("timestamp with time zone");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ChannelId", "ManagedNodePubKey")
+ .IsUnique();
+
+ b.ToTable("ChannelRoutingStates");
+ });
+
+ modelBuilder.Entity("NodeGuard.Data.Models.FMUTXO", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("CreationDatetime")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("OutputIndex")
+ .HasColumnType("bigint");
+
+ b.Property("SatsAmount")
+ .HasColumnType("bigint");
+
+ b.Property("TxId")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("UpdateDatetime")
+ .HasColumnType("timestamp with time zone");
+
+ b.HasKey("Id");
+
+ b.ToTable("FMUTXOs");
+ });
+
+ modelBuilder.Entity("NodeGuard.Data.Models.ForwardingHtlcEvent", b =>
+ {
+ b.Property("ManagedNodePubKey")
+ .HasColumnType("text");
+
+ b.Property("IncomingChannelId")
+ .HasColumnType("numeric(20,0)");
+
+ b.Property("OutgoingChannelId")
+ .HasColumnType("numeric(20,0)");
+
+ b.Property("IncomingHtlcId")
+ .HasColumnType("numeric(20,0)");
+
+ b.Property("OutgoingHtlcId")
+ .HasColumnType("numeric(20,0)");
+
+ b.Property("CreationDatetime")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("EventCase")
+ .HasColumnType("integer");
+
+ b.Property("EventTimestamp")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("EventType")
+ .HasColumnType("integer");
+
+ b.Property("FailureDetail")
+ .HasColumnType("integer");
+
+ b.Property("FailureString")
+ .HasMaxLength(2048)
+ .HasColumnType("character varying(2048)");
+
+ b.Property("FeeMsat")
+ .HasColumnType("bigint");
+
+ b.Property("GrossFeeMsat")
+ .HasColumnType("bigint");
+
+ b.Property("InboundFeeMsat")
+ .HasColumnType("bigint");
+
+ b.Property("InboundFeePpm")
+ .HasColumnType("bigint");
+
+ b.Property("IncomingAmountMsat")
+ .HasColumnType("numeric(20,0)");
+
+ b.Property("IncomingPeerAlias")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("IncomingTimelock")
+ .HasColumnType("bigint");
+
+ b.Property("ManagedNodeName")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("Outcome")
+ .HasColumnType("integer");
+
+ b.Property("OutgoingAmountMsat")
+ .HasColumnType("numeric(20,0)");
+
+ b.Property("OutgoingPeerAlias")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("OutgoingTimelock")
+ .HasColumnType("bigint");
+
+ b.Property("RoutingFeePpm")
+ .HasColumnType("bigint");
+
+ b.Property("UpdateDatetime")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("WireFailureCode")
+ .HasColumnType("integer");
+
+ b.HasKey("ManagedNodePubKey", "IncomingChannelId", "OutgoingChannelId", "IncomingHtlcId", "OutgoingHtlcId");
+
+ b.HasIndex("CreationDatetime");
+
+ b.HasIndex("EventTimestamp");
+
+ b.ToTable("ForwardingHtlcEvents");
+ });
+
+ modelBuilder.Entity("NodeGuard.Data.Models.InternalWallet", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("CreationDatetime")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("DerivationPath")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("MasterFingerprint")
+ .HasColumnType("text");
+
+ b.Property("MnemonicString")
+ .HasColumnType("text");
+
+ b.Property("UpdateDatetime")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("XPUB")
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.ToTable("InternalWallets");
+ });
+
+ modelBuilder.Entity("NodeGuard.Data.Models.Key", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("CreationDatetime")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Description")
+ .HasColumnType("text");
+
+ b.Property("InternalWalletId")
+ .HasColumnType("integer");
+
+ b.Property("IsArchived")
+ .HasColumnType("boolean");
+
+ b.Property("IsBIP39ImportedKey")
+ .HasColumnType("boolean");
+
+ b.Property("IsCompromised")
+ .HasColumnType("boolean");
+
+ b.Property("MasterFingerprint")
+ .HasColumnType("text");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Path")
+ .HasColumnType("text");
+
+ b.Property("UpdateDatetime")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UserId")
+ .HasColumnType("text");
+
+ b.Property("XPUB")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("InternalWalletId");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("Keys");
+ });
+
+ modelBuilder.Entity("NodeGuard.Data.Models.LiquidityRule", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("ChannelId")
+ .HasColumnType("integer");
+
+ b.Property("CreationDatetime")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("IsReverseSwapWalletRule")
+ .HasColumnType("boolean");
+
+ b.Property("MinimumLocalBalance")
+ .HasColumnType("numeric");
+
+ b.Property("MinimumRemoteBalance")
+ .HasColumnType("numeric");
+
+ b.Property("NodeId")
+ .HasColumnType("integer");
+
+ b.Property("RebalanceTarget")
+ .HasColumnType("numeric");
+
+ b.Property("ReverseSwapAddress")
+ .HasColumnType("text");
+
+ b.Property("ReverseSwapWalletId")
+ .HasColumnType("integer");
+
+ b.Property("SwapWalletId")
+ .HasColumnType("integer");
+
+ b.Property("UpdateDatetime")
+ .HasColumnType("timestamp with time zone");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ChannelId")
+ .IsUnique();
+
+ b.HasIndex("NodeId");
+
+ b.HasIndex("ReverseSwapWalletId");
+
+ b.HasIndex("SwapWalletId");
+
+ b.ToTable("LiquidityRules");
+ });
+
+ modelBuilder.Entity("NodeGuard.Data.Models.Node", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("AllowPositiveInboundFees")
+ .HasColumnType("boolean");
+
+ b.Property("AutoLiquidityManagementEnabled")
+ .HasColumnType("boolean");
+
+ b.Property("AutoRebalanceEnabled")
+ .HasColumnType("boolean");
+
+ b.Property("AutosweepEnabled")
+ .HasColumnType("boolean");
+
+ b.Property("ChannelAdminMacaroon")
+ .HasColumnType("text");
+
+ b.Property("CreationDatetime")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Description")
+ .HasColumnType("text");
+
+ b.Property("DynamicFeeManagementEnabled")
+ .HasColumnType("boolean");
+
+ b.Property("Endpoint")
+ .HasColumnType("text");
+
+ b.Property("FortySwapEndpoint")
+ .HasColumnType("text");
+
+ b.Property("FortySwapWeight")
+ .HasColumnType("integer");
+
+ b.Property("FundsDestinationWalletId")
+ .HasColumnType("integer");
+
+ b.Property("IsNodeDisabled")
+ .HasColumnType("boolean");
+
+ b.Property("LoopSwapWeight")
+ .HasColumnType("integer");
+
+ b.Property("LoopdCert")
+ .HasColumnType("text");
+
+ b.Property("LoopdEndpoint")
+ .HasColumnType("text");
+
+ b.Property("LoopdMacaroon")
+ .HasColumnType("text");
+
+ b.Property("MaxRebalanceCostToEarnRatio")
+ .HasColumnType("double precision");
+
+ b.Property("MaxRebalancesInFlight")
+ .HasColumnType("integer");
+
+ b.Property("MaxSwapRoutingFeeRatio")
+ .HasColumnType("numeric");
+
+ b.Property("MaxSwapsInFlight")
+ .HasColumnType("integer");
+
+ b.Property("MinimumBalanceThresholdSats")
+ .HasColumnType("bigint");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property