diff --git a/src/Data/Models/Rebalance.cs b/src/Data/Models/Rebalance.cs
index 381b0e09..f5d14db7 100644
--- a/src/Data/Models/Rebalance.cs
+++ b/src/Data/Models/Rebalance.cs
@@ -24,15 +24,16 @@ namespace NodeGuard.Data.Models;
public enum RebalanceStatus
{
- Pending,
- Probing,
- InFlight,
- Succeeded,
- Failed,
- NoRoute,
- Timeout,
- InsufficientBalance,
- ExceededFeeLimit
+ Pending = 0,
+ // 1 was Probing — removed (rebalances go straight to SendPaymentV2, no probe). The value is
+ // intentionally left as a gap so the other statuses keep their stored int values.
+ InFlight = 2,
+ Succeeded = 3,
+ Failed = 4,
+ NoRoute = 5,
+ Timeout = 6,
+ InsufficientBalance = 7,
+ ExceededFeeLimit = 8
}
public class Rebalance : Entity
@@ -132,12 +133,13 @@ public class Rebalance : Entity
public int TimeoutSeconds { get; set; } = 60;
///
- /// Multiplier applied to the probe amount after each failure. Range: (0, 1) exclusive.
- /// 0.5 = halve (default); 0.8 = next try is 20% smaller (finer-grained, more iterations);
- /// 0.3 = next try is 70% smaller (gives up faster). When null, the runtime falls back
- /// to Constants.REBALANCE_PROBE_BACKOFF_RATIO.
+ /// Multiplier applied to the rebalanced amount on each retry attempt. Range: (0, 1].
+ /// 1 = never shrink (every attempt retries the full requested amount); 0.8 = each retry is
+ /// 20% smaller; 0.5 = halve each time. The amount for attempt n is
+ /// RequestedAmountSats × ratio^(n-1), floored at Constants.REBALANCE_MIN_AMOUNT_SATS.
+ /// When null, the runtime falls back to Constants.REBALANCE_AMOUNT_BACKOFF_RATIO.
///
- public double? ProbeBackoffRatio { get; set; }
+ public double? AmountBackoffRatio { get; set; }
///
/// Maximum number of attempts (including the first try). When null, falls back to
diff --git a/src/Data/Repositories/Interfaces/IRebalanceRepository.cs b/src/Data/Repositories/Interfaces/IRebalanceRepository.cs
index d1bddaa9..9fe1babf 100644
--- a/src/Data/Repositories/Interfaces/IRebalanceRepository.cs
+++ b/src/Data/Repositories/Interfaces/IRebalanceRepository.cs
@@ -39,7 +39,7 @@ public interface IRebalanceRepository
///
/// Returns rebalances the monitor job should reconcile against LND:
- /// non-terminal rows (Pending/Probing/InFlight) plus recently-marked terminal failures
+ /// non-terminal rows (Pending/InFlight) plus recently-marked terminal failures
/// within . Only rows with a stored payment hash
/// are returned — without a hash there is nothing to look up in LND.
/// The recent-terminal sweep exists because the catch-all in RebalanceService can mark
diff --git a/src/Data/Repositories/RebalanceRepository.cs b/src/Data/Repositories/RebalanceRepository.cs
index 50fca33a..1c59997e 100644
--- a/src/Data/Repositories/RebalanceRepository.cs
+++ b/src/Data/Repositories/RebalanceRepository.cs
@@ -111,7 +111,6 @@ public async Task> GetReconcilable(TimeSpan recentTerminalWindow
.Include(r => r.Node)
.Where(r => r.PaymentHashHex != null
&& (r.Status == RebalanceStatus.Pending
- || r.Status == RebalanceStatus.Probing
|| r.Status == RebalanceStatus.InFlight
|| ((r.Status == RebalanceStatus.Failed
|| r.Status == RebalanceStatus.Timeout
diff --git a/src/Helpers/Constants.cs b/src/Helpers/Constants.cs
index 84b67ca7..4478b60f 100644
--- a/src/Helpers/Constants.cs
+++ b/src/Helpers/Constants.cs
@@ -179,22 +179,25 @@ public class Constants
public static double REBALANCE_RETRY_BACKOFF_MULTIPLIER = 2.0;
///
- /// Smallest amount the probing binary search will halve down to before giving up.
+ /// Smallest amount a rebalance attempt will shrink down to before giving up (the floor
+ /// for the per-attempt amount backoff).
///
- public static long REBALANCE_MIN_PROBE_AMOUNT_SATS = 10_000;
+ public static long REBALANCE_MIN_AMOUNT_SATS = 10_000;
///
- /// Multiplier applied to the probe amount after each failure. Range: (0, 1) exclusive.
- /// 0.5 (default) = halve the amount each iteration; 0.8 = next try is 20% smaller
- /// (more iterations, finer granularity); 0.3 = next try is 70% smaller (fewer
- /// iterations, gives up faster).
+ /// Multiplier applied to the rebalanced amount on each retry attempt. Range: (0, 1].
+ /// 1 = never shrink (every attempt retries the full requested amount); 0.8 = each retry
+ /// is 20% smaller than the previous attempt (partial rebalancing); 0.5 = halve each time.
+ /// The amount for attempt n is RequestedAmountSats × ratio^(n-1), floored at
+ /// REBALANCE_MIN_AMOUNT_SATS.
///
- public static double REBALANCE_PROBE_BACKOFF_RATIO = 0.8;
+ public static double REBALANCE_AMOUNT_BACKOFF_RATIO = 0.8;
///
- /// Maximum number of QueryRoutes-returned routes the prober will try per amount level.
+ /// 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.
///
- public static int REBALANCE_MAX_PROBE_ROUTES_PER_AMOUNT = 6;
+ public static uint REBALANCE_MAX_PARTS = 32;
///
/// How far back the MonitorRebalancesJob sweeps for already-terminal-but-possibly-wrong
@@ -428,14 +431,14 @@ static Constants()
var rebBackoff = Environment.GetEnvironmentVariable("REBALANCE_RETRY_BACKOFF_MULTIPLIER");
if (rebBackoff != null) REBALANCE_RETRY_BACKOFF_MULTIPLIER = double.Parse(rebBackoff, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture);
- var rebMinProbe = Environment.GetEnvironmentVariable("REBALANCE_MIN_PROBE_AMOUNT_SATS");
- if (rebMinProbe != null) REBALANCE_MIN_PROBE_AMOUNT_SATS = long.Parse(rebMinProbe);
+ var rebMinAmount = Environment.GetEnvironmentVariable("REBALANCE_MIN_AMOUNT_SATS");
+ if (rebMinAmount != null) REBALANCE_MIN_AMOUNT_SATS = long.Parse(rebMinAmount);
- var rebProbeBackoff = Environment.GetEnvironmentVariable("REBALANCE_PROBE_BACKOFF_RATIO");
- if (rebProbeBackoff != null) REBALANCE_PROBE_BACKOFF_RATIO = double.Parse(rebProbeBackoff, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture);
+ var rebProbeBackoff = Environment.GetEnvironmentVariable("REBALANCE_AMOUNT_BACKOFF_RATIO");
+ if (rebProbeBackoff != null) REBALANCE_AMOUNT_BACKOFF_RATIO = double.Parse(rebProbeBackoff, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture);
- var rebMaxProbeRoutes = Environment.GetEnvironmentVariable("REBALANCE_MAX_PROBE_ROUTES_PER_AMOUNT");
- if (rebMaxProbeRoutes != null) REBALANCE_MAX_PROBE_ROUTES_PER_AMOUNT = int.Parse(rebMaxProbeRoutes);
+ var rebMaxParts = Environment.GetEnvironmentVariable("REBALANCE_MAX_PARTS");
+ if (rebMaxParts != null) REBALANCE_MAX_PARTS = uint.Parse(rebMaxParts);
var rebReconcileWindow = Environment.GetEnvironmentVariable("REBALANCE_RECONCILE_TERMINAL_WINDOW_HOURS");
if (rebReconcileWindow != null) REBALANCE_RECONCILE_TERMINAL_WINDOW_HOURS = int.Parse(rebReconcileWindow);
diff --git a/src/Jobs/MonitorRebalancesJob.cs b/src/Jobs/MonitorRebalancesJob.cs
index 936f6388..dce79440 100644
--- a/src/Jobs/MonitorRebalancesJob.cs
+++ b/src/Jobs/MonitorRebalancesJob.cs
@@ -125,11 +125,11 @@ private async Task ReconcileAsync(Rebalance rebalance, CancellationToken ct)
// LND returned NotFound (or the call errored). The right action depends on which
// non-terminal state the row is in:
//
- // - Pending/Probing: the local ExecuteAsync persisted PaymentHashHex right after
- // AddInvoice, but SendPaymentV2 doesn't fire until after the probe succeeds —
- // so "no record in LND" is the EXPECTED state during this window. Flipping the
- // row to Failed here would kill an in-progress probe. Leave it alone; the local
- // execution is the source of truth until it dispatches SendPaymentV2.
+ // - Pending: the local ExecuteAsync persisted PaymentHashHex right after AddInvoice,
+ // but SendPaymentV2 hasn't fired yet — so "no record in LND" is the EXPECTED state
+ // during this window. Flipping the row to Failed here would kill an in-progress
+ // attempt. Leave it alone; the local execution is the source of truth until it
+ // dispatches SendPaymentV2.
//
// - InFlight: SendpaymentV2 has been invoked, so LND should know.
// If it doesn't, the payment never reached LND (process crashed mid-dispatch,
@@ -157,7 +157,7 @@ await _auditService.LogSystemAsync(
NewStatus = rebalance.Status.ToString(),
});
}
- else if (rebalance.Status is RebalanceStatus.Pending or RebalanceStatus.Probing)
+ else if (rebalance.Status is RebalanceStatus.Pending)
{
var window = TimeSpan.FromHours(Constants.REBALANCE_RECONCILE_TERMINAL_WINDOW_HOURS);
if (rebalance.UpdateDatetime < DateTimeOffset.UtcNow - window)
@@ -198,7 +198,7 @@ await _auditService.LogSystemAsync(
return;
}
- if (rebalance.Status is RebalanceStatus.Pending or RebalanceStatus.Probing)
+ if (rebalance.Status is RebalanceStatus.Pending)
{
_logger.LogDebug(
"Rebalance {RebalanceId} (status={Status}) has a payment record in LND (status={LndStatus}, hash={PaymentHashHex}); local execution still owns it, leaving as-is",
diff --git a/src/Migrations/20260630104437_RenameProbeBackoffRatioToAmountBackoffRatio.Designer.cs b/src/Migrations/20260630104437_RenameProbeBackoffRatioToAmountBackoffRatio.Designer.cs
new file mode 100644
index 00000000..2748969a
--- /dev/null
+++ b/src/Migrations/20260630104437_RenameProbeBackoffRatioToAmountBackoffRatio.Designer.cs
@@ -0,0 +1,1739 @@
+//
+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("20260630104437_RenameProbeBackoffRatioToAmountBackoffRatio")]
+ partial class RenameProbeBackoffRatioToAmountBackoffRatio
+ {
+ ///
+ 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("IsAutomatedLiquidityEnabled")
+ .HasColumnType("boolean");
+
+ 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.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.ToTable("ChannelOperationRequestPSBTs");
+ });
+
+ 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("AutoLiquidityManagementEnabled")
+ .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("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("MaxSwapRoutingFeeRatio")
+ .HasColumnType("numeric");
+
+ b.Property("MaxSwapsInFlight")
+ .HasColumnType("integer");
+
+ b.Property("MinimumBalanceThresholdSats")
+ .HasColumnType("bigint");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("PubKey")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("SwapBudgetRefreshInterval")
+ .HasColumnType("interval");
+
+ b.Property("SwapBudgetSats")
+ .HasColumnType("bigint");
+
+ b.Property("SwapBudgetStartDatetime")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("SwapMaxAmountSats")
+ .HasColumnType("bigint");
+
+ b.Property("SwapMinAmountSats")
+ .HasColumnType("bigint");
+
+ b.Property("UpdateDatetime")
+ .HasColumnType("timestamp with time zone");
+
+ b.HasKey("Id");
+
+ b.HasIndex("FundsDestinationWalletId");
+
+ b.HasIndex("PubKey")
+ .IsUnique();
+
+ b.ToTable("Nodes");
+ });
+
+ modelBuilder.Entity("NodeGuard.Data.Models.Rebalance", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("AmountBackoffRatio")
+ .HasColumnType("double precision");
+
+ b.Property("AttemptNumber")
+ .HasColumnType("integer");
+
+ b.Property("CreationDatetime")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("FeePaidMsat")
+ .HasColumnType("bigint");
+
+ b.Property("FeePaidSats")
+ .HasColumnType("bigint");
+
+ b.Property("IsManual")
+ .HasColumnType("boolean");
+
+ b.Property("MaxAttempts")
+ .HasColumnType("integer");
+
+ b.Property("MaxFeePct")
+ .HasColumnType("double precision");
+
+ b.Property("NodeId")
+ .HasColumnType("integer");
+
+ b.Property("PaymentHashHex")
+ .HasColumnType("text");
+
+ b.Property("PaymentRequest")
+ .HasColumnType("text");
+
+ b.Property("PreimageHex")
+ .HasColumnType("text");
+
+ b.Property("RequestedAmountSats")
+ .HasColumnType("bigint");
+
+ b.Property("RetryMaxFeePct")
+ .HasColumnType("double precision");
+
+ b.Property("SatsAmount")
+ .HasColumnType("bigint");
+
+ b.Property("SourceChanIdLnd")
+ .HasColumnType("numeric(20,0)");
+
+ b.Property("SourceChannelId")
+ .HasColumnType("integer");
+
+ b.Property("SourceNodePubKey")
+ .HasColumnType("text");
+
+ b.Property("Status")
+ .HasColumnType("integer");
+
+ b.Property("TargetPubkey")
+ .HasColumnType("text");
+
+ b.Property("TimeoutSeconds")
+ .HasColumnType("integer");
+
+ b.Property("UpdateDatetime")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UserRequestorId")
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("NodeId");
+
+ b.HasIndex("SourceChannelId");
+
+ b.HasIndex("UserRequestorId");
+
+ b.ToTable("Rebalances");
+ });
+
+ modelBuilder.Entity("NodeGuard.Data.Models.SwapOut", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("CreationDatetime")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("DestinationWalletId")
+ .HasColumnType("integer");
+
+ b.Property("ErrorDetails")
+ .HasColumnType("text");
+
+ b.Property("IsManual")
+ .HasColumnType("boolean");
+
+ b.Property("LightningFeeSats")
+ .HasColumnType("bigint");
+
+ b.Property("NodeId")
+ .HasColumnType("integer");
+
+ b.Property