From 6807ebade77835b6005bc9014fee1d3368f8d72a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20A=2EP?= <53834183+Jossec101@users.noreply.github.com> Date: Wed, 11 Jun 2025 16:34:03 +0200 Subject: [PATCH 01/16] Add migration for legacy withdrawal data and update model snapshot - Implemented migration to transfer existing data from WalletWithdrawalRequests to WalletWithdrawalRequestDestinations. - Added verification step to ensure data integrity during migration. --- ...43_MigrateLegacyWithdrawalData.Designer.cs | 1342 +++++++++++++++++ ...50611142943_MigrateLegacyWithdrawalData.cs | 84 ++ .../ApplicationDbContextModelSnapshot.cs | 36 +- 3 files changed, 1444 insertions(+), 18 deletions(-) create mode 100644 src/Migrations/20250611142943_MigrateLegacyWithdrawalData.Designer.cs create mode 100644 src/Migrations/20250611142943_MigrateLegacyWithdrawalData.cs diff --git a/src/Migrations/20250611142943_MigrateLegacyWithdrawalData.Designer.cs b/src/Migrations/20250611142943_MigrateLegacyWithdrawalData.Designer.cs new file mode 100644 index 00000000..6851f9f3 --- /dev/null +++ b/src/Migrations/20250611142943_MigrateLegacyWithdrawalData.Designer.cs @@ -0,0 +1,1342 @@ +// +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("20250611142943_MigrateLegacyWithdrawalData")] + partial class MigrateLegacyWithdrawalData + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.6") + .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("Discriminator").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.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("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("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.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("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("IsNodeDisabled") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PubKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReturningFundsWalletId") + .HasColumnType("integer"); + + b.Property("UpdateDatetime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("PubKey") + .IsUnique(); + + b.HasIndex("ReturningFundsWalletId"); + + b.ToTable("Nodes"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.UTXOTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreationDatetime") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("Outpoint") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdateDatetime") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Key", "Outpoint") + .IsUnique(); + + b.ToTable("UTXOTags"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.Wallet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BIP39Seedphrase") + .HasColumnType("text"); + + b.Property("CreationDatetime") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ImportedOutputDescriptor") + .HasColumnType("text"); + + b.Property("InternalWalletId") + .HasColumnType("integer"); + + b.Property("InternalWalletMasterFingerprint") + .HasColumnType("text"); + + b.Property("InternalWalletSubDerivationPath") + .HasColumnType("text"); + + b.Property("IsArchived") + .HasColumnType("boolean"); + + b.Property("IsBIP39Imported") + .HasColumnType("boolean"); + + b.Property("IsCompromised") + .HasColumnType("boolean"); + + b.Property("IsFinalised") + .HasColumnType("boolean"); + + b.Property("IsHotWallet") + .HasColumnType("boolean"); + + b.Property("IsUnSortedMultiSig") + .HasColumnType("boolean"); + + b.Property("MofN") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReferenceId") + .HasColumnType("text"); + + b.Property("UpdateDatetime") + .HasColumnType("timestamp with time zone"); + + b.Property("WalletAddressType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("InternalWalletId"); + + b.HasIndex("InternalWalletSubDerivationPath", "InternalWalletMasterFingerprint") + .IsUnique(); + + b.ToTable("Wallets"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.WalletWithdrawalRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasColumnType("numeric"); + + b.Property("Changeless") + .HasColumnType("boolean"); + + b.Property("CreationDatetime") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomFeeRate") + .HasColumnType("numeric"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("DestinationAddress") + .IsRequired() + .HasColumnType("text"); + + b.Property("MempoolRecommendedFeesType") + .HasColumnType("integer"); + + b.Property("ReferenceId") + .HasColumnType("text"); + + b.Property("RejectCancelDescription") + .HasColumnType("text"); + + b.Property("RequestMetadata") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TxId") + .HasColumnType("text"); + + b.Property("UpdateDatetime") + .HasColumnType("timestamp with time zone"); + + b.Property("UserRequestorId") + .HasColumnType("text"); + + b.Property("WalletId") + .HasColumnType("integer"); + + b.Property("WithdrawAllFunds") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("UserRequestorId"); + + b.HasIndex("WalletId"); + + b.ToTable("WalletWithdrawalRequests"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.WalletWithdrawalRequestDestination", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Address") + .IsRequired() + .HasColumnType("text"); + + b.Property("Amount") + .HasColumnType("numeric"); + + b.Property("CreationDatetime") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdateDatetime") + .HasColumnType("timestamp with time zone"); + + b.Property("WalletWithdrawalRequestId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("WalletWithdrawalRequestId"); + + b.ToTable("WalletWithdrawalRequestDestinations"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.WalletWithdrawalRequestPSBT", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + 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("SignerId") + .HasColumnType("text"); + + b.Property("UpdateDatetime") + .HasColumnType("timestamp with time zone"); + + b.Property("WalletWithdrawalRequestId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SignerId"); + + b.HasIndex("WalletWithdrawalRequestId"); + + b.ToTable("WalletWithdrawalRequestPSBTs"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.ApplicationUser", b => + { + b.HasBaseType("Microsoft.AspNetCore.Identity.IdentityUser"); + + b.HasDiscriminator().HasValue("ApplicationUser"); + }); + + modelBuilder.Entity("ApplicationUserNode", b => + { + b.HasOne("NodeGuard.Data.Models.Node", null) + .WithMany() + .HasForeignKey("NodesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("NodeGuard.Data.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UsersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ChannelOperationRequestFMUTXO", b => + { + b.HasOne("NodeGuard.Data.Models.ChannelOperationRequest", null) + .WithMany() + .HasForeignKey("ChannelOperationRequestsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("NodeGuard.Data.Models.FMUTXO", null) + .WithMany() + .HasForeignKey("UtxosId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("FMUTXOWalletWithdrawalRequest", b => + { + b.HasOne("NodeGuard.Data.Models.FMUTXO", null) + .WithMany() + .HasForeignKey("UTXOsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("NodeGuard.Data.Models.WalletWithdrawalRequest", null) + .WithMany() + .HasForeignKey("WalletWithdrawalRequestsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("KeyWallet", b => + { + b.HasOne("NodeGuard.Data.Models.Key", null) + .WithMany() + .HasForeignKey("KeysId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("NodeGuard.Data.Models.Wallet", null) + .WithMany() + .HasForeignKey("WalletsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.APIToken", b => + { + b.HasOne("NodeGuard.Data.Models.ApplicationUser", "Creator") + .WithMany() + .HasForeignKey("CreatorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Creator"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.Channel", b => + { + b.HasOne("NodeGuard.Data.Models.Node", "DestinationNode") + .WithMany() + .HasForeignKey("DestinationNodeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("NodeGuard.Data.Models.Node", "SourceNode") + .WithMany() + .HasForeignKey("SourceNodeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DestinationNode"); + + b.Navigation("SourceNode"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.ChannelOperationRequest", b => + { + b.HasOne("NodeGuard.Data.Models.Channel", "Channel") + .WithMany("ChannelOperationRequests") + .HasForeignKey("ChannelId"); + + b.HasOne("NodeGuard.Data.Models.Node", "DestNode") + .WithMany("ChannelOperationRequestsAsDestination") + .HasForeignKey("DestNodeId"); + + b.HasOne("NodeGuard.Data.Models.Node", "SourceNode") + .WithMany("ChannelOperationRequestsAsSource") + .HasForeignKey("SourceNodeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("NodeGuard.Data.Models.ApplicationUser", "User") + .WithMany("ChannelOperationRequests") + .HasForeignKey("UserId"); + + b.HasOne("NodeGuard.Data.Models.Wallet", "Wallet") + .WithMany("ChannelOperationRequestsAsSource") + .HasForeignKey("WalletId"); + + b.Navigation("Channel"); + + b.Navigation("DestNode"); + + b.Navigation("SourceNode"); + + b.Navigation("User"); + + b.Navigation("Wallet"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.ChannelOperationRequestPSBT", b => + { + b.HasOne("NodeGuard.Data.Models.ChannelOperationRequest", "ChannelOperationRequest") + .WithMany("ChannelOperationRequestPsbts") + .HasForeignKey("ChannelOperationRequestId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("NodeGuard.Data.Models.ApplicationUser", "UserSigner") + .WithMany() + .HasForeignKey("UserSignerId"); + + b.Navigation("ChannelOperationRequest"); + + b.Navigation("UserSigner"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.Key", b => + { + b.HasOne("NodeGuard.Data.Models.InternalWallet", "InternalWallet") + .WithMany() + .HasForeignKey("InternalWalletId"); + + b.HasOne("NodeGuard.Data.Models.ApplicationUser", "User") + .WithMany("Keys") + .HasForeignKey("UserId"); + + b.Navigation("InternalWallet"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.LiquidityRule", b => + { + b.HasOne("NodeGuard.Data.Models.Channel", "Channel") + .WithMany("LiquidityRules") + .HasForeignKey("ChannelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("NodeGuard.Data.Models.Node", "Node") + .WithMany() + .HasForeignKey("NodeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("NodeGuard.Data.Models.Wallet", "ReverseSwapWallet") + .WithMany("LiquidityRulesAsReverseSwapWallet") + .HasForeignKey("ReverseSwapWalletId"); + + b.HasOne("NodeGuard.Data.Models.Wallet", "SwapWallet") + .WithMany("LiquidityRulesAsSwapWallet") + .HasForeignKey("SwapWalletId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Channel"); + + b.Navigation("Node"); + + b.Navigation("ReverseSwapWallet"); + + b.Navigation("SwapWallet"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.Node", b => + { + b.HasOne("NodeGuard.Data.Models.Wallet", "ReturningFundsWallet") + .WithMany() + .HasForeignKey("ReturningFundsWalletId"); + + b.Navigation("ReturningFundsWallet"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.Wallet", b => + { + b.HasOne("NodeGuard.Data.Models.InternalWallet", "InternalWallet") + .WithMany() + .HasForeignKey("InternalWalletId"); + + b.Navigation("InternalWallet"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.WalletWithdrawalRequest", b => + { + b.HasOne("NodeGuard.Data.Models.ApplicationUser", "UserRequestor") + .WithMany("WalletWithdrawalRequests") + .HasForeignKey("UserRequestorId"); + + b.HasOne("NodeGuard.Data.Models.Wallet", "Wallet") + .WithMany() + .HasForeignKey("WalletId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("UserRequestor"); + + b.Navigation("Wallet"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.WalletWithdrawalRequestDestination", b => + { + b.HasOne("NodeGuard.Data.Models.WalletWithdrawalRequest", "WalletWithdrawalRequest") + .WithMany("WalletWithdrawalRequestDestinations") + .HasForeignKey("WalletWithdrawalRequestId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("WalletWithdrawalRequest"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.WalletWithdrawalRequestPSBT", b => + { + b.HasOne("NodeGuard.Data.Models.ApplicationUser", "Signer") + .WithMany() + .HasForeignKey("SignerId"); + + b.HasOne("NodeGuard.Data.Models.WalletWithdrawalRequest", "WalletWithdrawalRequest") + .WithMany("WalletWithdrawalRequestPSBTs") + .HasForeignKey("WalletWithdrawalRequestId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Signer"); + + b.Navigation("WalletWithdrawalRequest"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.Channel", b => + { + b.Navigation("ChannelOperationRequests"); + + b.Navigation("LiquidityRules"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.ChannelOperationRequest", b => + { + b.Navigation("ChannelOperationRequestPsbts"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.Node", b => + { + b.Navigation("ChannelOperationRequestsAsDestination"); + + b.Navigation("ChannelOperationRequestsAsSource"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.Wallet", b => + { + b.Navigation("ChannelOperationRequestsAsSource"); + + b.Navigation("LiquidityRulesAsReverseSwapWallet"); + + b.Navigation("LiquidityRulesAsSwapWallet"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.WalletWithdrawalRequest", b => + { + b.Navigation("WalletWithdrawalRequestDestinations"); + + b.Navigation("WalletWithdrawalRequestPSBTs"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.ApplicationUser", b => + { + b.Navigation("ChannelOperationRequests"); + + b.Navigation("Keys"); + + b.Navigation("WalletWithdrawalRequests"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Migrations/20250611142943_MigrateLegacyWithdrawalData.cs b/src/Migrations/20250611142943_MigrateLegacyWithdrawalData.cs new file mode 100644 index 00000000..b31497f9 --- /dev/null +++ b/src/Migrations/20250611142943_MigrateLegacyWithdrawalData.cs @@ -0,0 +1,84 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace NodeGuard.Migrations +{ + /// + public partial class MigrateLegacyWithdrawalData : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + // Execute data migration in an explicit transaction + migrationBuilder.Sql(@" + BEGIN; + + -- Step 1: Migrate existing data from Amount and DestinationAddress to WalletWithdrawalRequestDestinations + INSERT INTO ""WalletWithdrawalRequestDestinations"" (""Amount"", ""Address"", ""WalletWithdrawalRequestId"", ""CreationDatetime"", ""UpdateDatetime"") + SELECT + ""Amount"", + ""DestinationAddress"", + ""Id"", + ""CreationDatetime"", + ""UpdateDatetime"" + FROM ""WalletWithdrawalRequests"" + WHERE ""Amount"" > 0 AND ""DestinationAddress"" IS NOT NULL AND ""DestinationAddress"" != ''; + + -- Step 2: Verify migration was successful - if any records failed to migrate, rollback + DO $$ + DECLARE + original_count INTEGER; + migrated_count INTEGER; + BEGIN + -- Count original records that should have been migrated + SELECT COUNT(*) INTO original_count + FROM ""WalletWithdrawalRequests"" + WHERE ""Amount"" > 0 AND ""DestinationAddress"" IS NOT NULL AND ""DestinationAddress"" != ''; + + -- Count migrated records that were just inserted + SELECT COUNT(*) INTO migrated_count + FROM ""WalletWithdrawalRequestDestinations"" dest + INNER JOIN ""WalletWithdrawalRequests"" req ON dest.""WalletWithdrawalRequestId"" = req.""Id"" + WHERE req.""Amount"" > 0 AND req.""DestinationAddress"" IS NOT NULL AND req.""DestinationAddress"" != ''; + + -- If counts don't match, raise an exception to rollback the transaction + IF original_count != migrated_count THEN + RAISE EXCEPTION 'Data migration failed: Expected % records, but migrated % records. Transaction will be rolled back.', original_count, migrated_count; + END IF; + + -- Log success + RAISE NOTICE 'Successfully migrated % legacy withdrawal records to destinations table', original_count; + END $$; + + COMMIT; + "); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + // Rollback: Delete the migrated destinations in an explicit transaction + migrationBuilder.Sql(@" + BEGIN; + + -- Delete only the migrated destinations (those that match legacy withdrawal requests) + DELETE FROM ""WalletWithdrawalRequestDestinations"" + WHERE ""WalletWithdrawalRequestId"" IN ( + SELECT ""Id"" FROM ""WalletWithdrawalRequests"" + WHERE ""Amount"" > 0 AND ""DestinationAddress"" IS NOT NULL AND ""DestinationAddress"" != '' + ) + AND ""Address"" IN ( + SELECT ""DestinationAddress"" FROM ""WalletWithdrawalRequests"" + WHERE ""Amount"" > 0 AND ""DestinationAddress"" IS NOT NULL AND ""DestinationAddress"" != '' + ) + AND ""Amount"" IN ( + SELECT ""Amount"" FROM ""WalletWithdrawalRequests"" + WHERE ""Amount"" > 0 AND ""DestinationAddress"" IS NOT NULL AND ""DestinationAddress"" != '' + ); + + COMMIT; + "); + } + } +} diff --git a/src/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Migrations/ApplicationDbContextModelSnapshot.cs index 6c50fee8..c2628e18 100644 --- a/src/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Migrations/ApplicationDbContextModelSnapshot.cs @@ -36,7 +36,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("UsersId"); - b.ToTable("ApplicationUserNode"); + b.ToTable("ApplicationUserNode", (string)null); }); modelBuilder.Entity("ChannelOperationRequestFMUTXO", b => @@ -51,7 +51,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("UtxosId"); - b.ToTable("ChannelOperationRequestFMUTXO"); + b.ToTable("ChannelOperationRequestFMUTXO", (string)null); }); modelBuilder.Entity("FMUTXOWalletWithdrawalRequest", b => @@ -66,7 +66,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("WalletWithdrawalRequestsId"); - b.ToTable("FMUTXOWalletWithdrawalRequest"); + b.ToTable("FMUTXOWalletWithdrawalRequest", (string)null); }); modelBuilder.Entity("KeyWallet", b => @@ -81,7 +81,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("WalletsId"); - b.ToTable("KeyWallet"); + b.ToTable("KeyWallet", (string)null); }); modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => @@ -329,7 +329,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("CreatorId"); - b.ToTable("ApiTokens"); + b.ToTable("ApiTokens", (string)null); }); modelBuilder.Entity("NodeGuard.Data.Models.Channel", b => @@ -386,7 +386,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("SourceNodeId"); - b.ToTable("Channels"); + b.ToTable("Channels", (string)null); }); modelBuilder.Entity("NodeGuard.Data.Models.ChannelOperationRequest", b => @@ -466,7 +466,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("WalletId"); - b.ToTable("ChannelOperationRequests"); + b.ToTable("ChannelOperationRequests", (string)null); }); modelBuilder.Entity("NodeGuard.Data.Models.ChannelOperationRequestPSBT", b => @@ -508,7 +508,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("UserSignerId"); - b.ToTable("ChannelOperationRequestPSBTs"); + b.ToTable("ChannelOperationRequestPSBTs", (string)null); }); modelBuilder.Entity("NodeGuard.Data.Models.FMUTXO", b => @@ -537,7 +537,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); - b.ToTable("FMUTXOs"); + b.ToTable("FMUTXOs", (string)null); }); modelBuilder.Entity("NodeGuard.Data.Models.InternalWallet", b => @@ -569,7 +569,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); - b.ToTable("InternalWallets"); + b.ToTable("InternalWallets", (string)null); }); modelBuilder.Entity("NodeGuard.Data.Models.Key", b => @@ -624,7 +624,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("UserId"); - b.ToTable("Keys"); + b.ToTable("Keys", (string)null); }); modelBuilder.Entity("NodeGuard.Data.Models.LiquidityRule", b => @@ -679,7 +679,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("SwapWalletId"); - b.ToTable("LiquidityRules"); + b.ToTable("LiquidityRules", (string)null); }); modelBuilder.Entity("NodeGuard.Data.Models.Node", b => @@ -729,7 +729,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("ReturningFundsWalletId"); - b.ToTable("Nodes"); + b.ToTable("Nodes", (string)null); }); modelBuilder.Entity("NodeGuard.Data.Models.UTXOTag", b => @@ -763,7 +763,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("Key", "Outpoint") .IsUnique(); - b.ToTable("UTXOTags"); + b.ToTable("UTXOTags", (string)null); }); modelBuilder.Entity("NodeGuard.Data.Models.Wallet", b => @@ -836,7 +836,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("InternalWalletSubDerivationPath", "InternalWalletMasterFingerprint") .IsUnique(); - b.ToTable("Wallets"); + b.ToTable("Wallets", (string)null); }); modelBuilder.Entity("NodeGuard.Data.Models.WalletWithdrawalRequest", b => @@ -903,7 +903,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("WalletId"); - b.ToTable("WalletWithdrawalRequests"); + b.ToTable("WalletWithdrawalRequests", (string)null); }); modelBuilder.Entity("NodeGuard.Data.Models.WalletWithdrawalRequestDestination", b => @@ -934,7 +934,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("WalletWithdrawalRequestId"); - b.ToTable("WalletWithdrawalRequestDestinations"); + b.ToTable("WalletWithdrawalRequestDestinations", (string)null); }); modelBuilder.Entity("NodeGuard.Data.Models.WalletWithdrawalRequestPSBT", b => @@ -976,7 +976,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("WalletWithdrawalRequestId"); - b.ToTable("WalletWithdrawalRequestPSBTs"); + b.ToTable("WalletWithdrawalRequestPSBTs", (string)null); }); modelBuilder.Entity("NodeGuard.Data.Models.ApplicationUser", b => From 105a44f10a37a4390128c909973cc7648aba2938 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20A=2EP?= <53834183+Jossec101@users.noreply.github.com> Date: Wed, 11 Jun 2025 16:45:36 +0200 Subject: [PATCH 02/16] Error handling in database migration and clean up whitespace in DbInitializer --- src/Data/DbInitializer.cs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/Data/DbInitializer.cs b/src/Data/DbInitializer.cs index f12816d5..1255e595 100644 --- a/src/Data/DbInitializer.cs +++ b/src/Data/DbInitializer.cs @@ -74,12 +74,13 @@ public static void Initialize(IServiceProvider serviceProvider) { applicationDbContext.Database.Migrate(); dataprotectionKeysContext.Database.Migrate(); - + isConnected = true; } catch (Exception e) { logger.LogError(e, "Error while migrating"); + throw new Exception("Error while migrating the database", e); } Thread.Sleep(1_000); @@ -327,7 +328,7 @@ public static void Initialize(IServiceProvider serviceProvider) var lastEventId = evts.LastEventId; logger?.LogInformation(lastEventId.ToString()); - + minerRPC.SendToAddress(legacyMultisigAddress, legacyMultisigFundCoins); minerRPC.SendToAddress(multisigAddress, multisigFundCoins); minerRPC.SendToAddress(singlesigAddress, singlesigFundCoins); @@ -335,7 +336,7 @@ public static void Initialize(IServiceProvider serviceProvider) //6 blocks to confirm minerRPC.Generate(6); - + var notification1 = WaitNbxplorerNotification(evts, legacyMultisigDerivationStrategy, lastEventId); var notification2 = WaitNbxplorerNotification(evts, multisigDerivationStrategy, lastEventId); var notification3 = WaitNbxplorerNotification(evts, singlesigDerivationStrategy, lastEventId); @@ -345,7 +346,7 @@ public static void Initialize(IServiceProvider serviceProvider) { throw new Exception("Wallets are not initialized"); } - + var legacyMultisigBalance = nbxplorerClient.GetBalance(legacyMultisigDerivationStrategy); var multisigBalance = nbxplorerClient.GetBalance(multisigDerivationStrategy); var singleSigbalance = nbxplorerClient.GetBalance(singlesigDerivationStrategy); @@ -376,7 +377,7 @@ public static void Initialize(IServiceProvider serviceProvider) applicationDbContext.Add(testingSinglesigWallet); applicationDbContext.Add(testingSingleSigBIP39Wallet); } - + // API Tokens generation for services var authenticatedServices = new Dictionary { @@ -385,10 +386,10 @@ public static void Initialize(IServiceProvider serviceProvider) { "Liquidator", "8rvSsUGeyXXdDQrHctcTey/xtHdZQEn945KHwccKp9Q=" }, { "ElenPay", "2otDr3IrdARnarQZU8RO0ImQko6CDICyLmGUflKUQWA=" } }; - + var existingTokens = applicationDbContext.ApiTokens.Where(token => authenticatedServices.Keys.Contains(token.Name)).ToList(); - + if (existingTokens.Count != authenticatedServices.Count && adminUser != null) { foreach (var service in authenticatedServices) @@ -462,7 +463,7 @@ private static APIToken CreateApiToken(string name, string token, string userId) IsBlocked = false, CreatorId = userId }; - + apiToken.SetCreationDatetime(); apiToken.SetUpdateDatetime(); From 6dd9ccdb1ac632b1e4dedf426e0bb6d1be8cdc0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20A=2EP?= <53834183+Jossec101@users.noreply.github.com> Date: Wed, 11 Jun 2025 17:49:27 +0200 Subject: [PATCH 03/16] Remove obsolete properties from WalletWithdrawalRequest model and update migration files --- NodeGuard.code-workspace | 11 + src/Data/Models/WalletWithdrawalRequest.cs | 28 +- ...veObsoleteWithdrawalProperties.Designer.cs | 1335 +++++++++++++++++ ...4856_RemoveObsoleteWithdrawalProperties.cs | 40 + 4 files changed, 1391 insertions(+), 23 deletions(-) create mode 100644 NodeGuard.code-workspace create mode 100644 src/Migrations/20250611154856_RemoveObsoleteWithdrawalProperties.Designer.cs create mode 100644 src/Migrations/20250611154856_RemoveObsoleteWithdrawalProperties.cs diff --git a/NodeGuard.code-workspace b/NodeGuard.code-workspace new file mode 100644 index 00000000..30d086ea --- /dev/null +++ b/NodeGuard.code-workspace @@ -0,0 +1,11 @@ +{ + "folders": [ + { + "path": "." + }, + { + "path": "src" + } + ], + "settings": {} +} \ No newline at end of file diff --git a/src/Data/Models/WalletWithdrawalRequest.cs b/src/Data/Models/WalletWithdrawalRequest.cs index 0a3cdd76..c56b124a 100644 --- a/src/Data/Models/WalletWithdrawalRequest.cs +++ b/src/Data/Models/WalletWithdrawalRequest.cs @@ -73,12 +73,6 @@ public class WalletWithdrawalRequest : Entity, IEquatable - /// base58 address of the output of the request, its mutually exclusive with the WalletWithdrawalRequestDestinations - /// - public string DestinationAddress { get; set; } - /// /// Description by the requestor /// @@ -91,28 +85,12 @@ public class WalletWithdrawalRequest : Entity, IEquatable - /// TX amount in BTC, its mutually exclusive with the WalletWithdrawalRequestDestinations - /// - /// - public decimal Amount { get; set; } - /// /// Checks if all the threshold signatures are collected, including the internal wallet key (even if not signed yet) /// [NotMapped] public bool AreAllRequiredHumanSignaturesCollected => CheckSignatures(); - [NotMapped] - /// - /// This indicates if the request is a legacy request, meaning that it has no destinations and only a single address and amount - /// -#pragma warning disable CS0618 // Type or member is obsolete - public bool IsLegacy => Amount > 0 && string.IsNullOrEmpty(DestinationAddress) && - (WalletWithdrawalRequestDestinations == null || WalletWithdrawalRequestDestinations?.Count == 0); -#pragma warning restore CS0618 // Type or member is obsolete - [NotMapped] public int NumberOfSignaturesCollected => WalletWithdrawalRequestPSBTs == null @@ -193,7 +171,11 @@ public bool Equals(WalletWithdrawalRequest? other) } [NotMapped] - public long SatsAmount => new Money(Amount, MoneyUnit.BTC).Satoshi; + public decimal Amount => + WalletWithdrawalRequestDestinations?.Sum(x => x.Amount) ?? 0m; + + [NotMapped] + public long SatsAmount => new Money(WalletWithdrawalRequestDestinations?.Sum(x => x.Amount) ?? 0m, MoneyUnit.BTC).Satoshi; #region Relationships diff --git a/src/Migrations/20250611154856_RemoveObsoleteWithdrawalProperties.Designer.cs b/src/Migrations/20250611154856_RemoveObsoleteWithdrawalProperties.Designer.cs new file mode 100644 index 00000000..ddd734a2 --- /dev/null +++ b/src/Migrations/20250611154856_RemoveObsoleteWithdrawalProperties.Designer.cs @@ -0,0 +1,1335 @@ +// +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("20250611154856_RemoveObsoleteWithdrawalProperties")] + partial class RemoveObsoleteWithdrawalProperties + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.6") + .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("Discriminator").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.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("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("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.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("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("IsNodeDisabled") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PubKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReturningFundsWalletId") + .HasColumnType("integer"); + + b.Property("UpdateDatetime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("PubKey") + .IsUnique(); + + b.HasIndex("ReturningFundsWalletId"); + + b.ToTable("Nodes"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.UTXOTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreationDatetime") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("Outpoint") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdateDatetime") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Key", "Outpoint") + .IsUnique(); + + b.ToTable("UTXOTags"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.Wallet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BIP39Seedphrase") + .HasColumnType("text"); + + b.Property("CreationDatetime") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ImportedOutputDescriptor") + .HasColumnType("text"); + + b.Property("InternalWalletId") + .HasColumnType("integer"); + + b.Property("InternalWalletMasterFingerprint") + .HasColumnType("text"); + + b.Property("InternalWalletSubDerivationPath") + .HasColumnType("text"); + + b.Property("IsArchived") + .HasColumnType("boolean"); + + b.Property("IsBIP39Imported") + .HasColumnType("boolean"); + + b.Property("IsCompromised") + .HasColumnType("boolean"); + + b.Property("IsFinalised") + .HasColumnType("boolean"); + + b.Property("IsHotWallet") + .HasColumnType("boolean"); + + b.Property("IsUnSortedMultiSig") + .HasColumnType("boolean"); + + b.Property("MofN") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReferenceId") + .HasColumnType("text"); + + b.Property("UpdateDatetime") + .HasColumnType("timestamp with time zone"); + + b.Property("WalletAddressType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("InternalWalletId"); + + b.HasIndex("InternalWalletSubDerivationPath", "InternalWalletMasterFingerprint") + .IsUnique(); + + b.ToTable("Wallets"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.WalletWithdrawalRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Changeless") + .HasColumnType("boolean"); + + b.Property("CreationDatetime") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomFeeRate") + .HasColumnType("numeric"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("MempoolRecommendedFeesType") + .HasColumnType("integer"); + + b.Property("ReferenceId") + .HasColumnType("text"); + + b.Property("RejectCancelDescription") + .HasColumnType("text"); + + b.Property("RequestMetadata") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TxId") + .HasColumnType("text"); + + b.Property("UpdateDatetime") + .HasColumnType("timestamp with time zone"); + + b.Property("UserRequestorId") + .HasColumnType("text"); + + b.Property("WalletId") + .HasColumnType("integer"); + + b.Property("WithdrawAllFunds") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("UserRequestorId"); + + b.HasIndex("WalletId"); + + b.ToTable("WalletWithdrawalRequests"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.WalletWithdrawalRequestDestination", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Address") + .IsRequired() + .HasColumnType("text"); + + b.Property("Amount") + .HasColumnType("numeric"); + + b.Property("CreationDatetime") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdateDatetime") + .HasColumnType("timestamp with time zone"); + + b.Property("WalletWithdrawalRequestId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("WalletWithdrawalRequestId"); + + b.ToTable("WalletWithdrawalRequestDestinations"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.WalletWithdrawalRequestPSBT", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + 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("SignerId") + .HasColumnType("text"); + + b.Property("UpdateDatetime") + .HasColumnType("timestamp with time zone"); + + b.Property("WalletWithdrawalRequestId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SignerId"); + + b.HasIndex("WalletWithdrawalRequestId"); + + b.ToTable("WalletWithdrawalRequestPSBTs"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.ApplicationUser", b => + { + b.HasBaseType("Microsoft.AspNetCore.Identity.IdentityUser"); + + b.HasDiscriminator().HasValue("ApplicationUser"); + }); + + modelBuilder.Entity("ApplicationUserNode", b => + { + b.HasOne("NodeGuard.Data.Models.Node", null) + .WithMany() + .HasForeignKey("NodesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("NodeGuard.Data.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UsersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ChannelOperationRequestFMUTXO", b => + { + b.HasOne("NodeGuard.Data.Models.ChannelOperationRequest", null) + .WithMany() + .HasForeignKey("ChannelOperationRequestsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("NodeGuard.Data.Models.FMUTXO", null) + .WithMany() + .HasForeignKey("UtxosId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("FMUTXOWalletWithdrawalRequest", b => + { + b.HasOne("NodeGuard.Data.Models.FMUTXO", null) + .WithMany() + .HasForeignKey("UTXOsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("NodeGuard.Data.Models.WalletWithdrawalRequest", null) + .WithMany() + .HasForeignKey("WalletWithdrawalRequestsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("KeyWallet", b => + { + b.HasOne("NodeGuard.Data.Models.Key", null) + .WithMany() + .HasForeignKey("KeysId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("NodeGuard.Data.Models.Wallet", null) + .WithMany() + .HasForeignKey("WalletsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.APIToken", b => + { + b.HasOne("NodeGuard.Data.Models.ApplicationUser", "Creator") + .WithMany() + .HasForeignKey("CreatorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Creator"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.Channel", b => + { + b.HasOne("NodeGuard.Data.Models.Node", "DestinationNode") + .WithMany() + .HasForeignKey("DestinationNodeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("NodeGuard.Data.Models.Node", "SourceNode") + .WithMany() + .HasForeignKey("SourceNodeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DestinationNode"); + + b.Navigation("SourceNode"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.ChannelOperationRequest", b => + { + b.HasOne("NodeGuard.Data.Models.Channel", "Channel") + .WithMany("ChannelOperationRequests") + .HasForeignKey("ChannelId"); + + b.HasOne("NodeGuard.Data.Models.Node", "DestNode") + .WithMany("ChannelOperationRequestsAsDestination") + .HasForeignKey("DestNodeId"); + + b.HasOne("NodeGuard.Data.Models.Node", "SourceNode") + .WithMany("ChannelOperationRequestsAsSource") + .HasForeignKey("SourceNodeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("NodeGuard.Data.Models.ApplicationUser", "User") + .WithMany("ChannelOperationRequests") + .HasForeignKey("UserId"); + + b.HasOne("NodeGuard.Data.Models.Wallet", "Wallet") + .WithMany("ChannelOperationRequestsAsSource") + .HasForeignKey("WalletId"); + + b.Navigation("Channel"); + + b.Navigation("DestNode"); + + b.Navigation("SourceNode"); + + b.Navigation("User"); + + b.Navigation("Wallet"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.ChannelOperationRequestPSBT", b => + { + b.HasOne("NodeGuard.Data.Models.ChannelOperationRequest", "ChannelOperationRequest") + .WithMany("ChannelOperationRequestPsbts") + .HasForeignKey("ChannelOperationRequestId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("NodeGuard.Data.Models.ApplicationUser", "UserSigner") + .WithMany() + .HasForeignKey("UserSignerId"); + + b.Navigation("ChannelOperationRequest"); + + b.Navigation("UserSigner"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.Key", b => + { + b.HasOne("NodeGuard.Data.Models.InternalWallet", "InternalWallet") + .WithMany() + .HasForeignKey("InternalWalletId"); + + b.HasOne("NodeGuard.Data.Models.ApplicationUser", "User") + .WithMany("Keys") + .HasForeignKey("UserId"); + + b.Navigation("InternalWallet"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.LiquidityRule", b => + { + b.HasOne("NodeGuard.Data.Models.Channel", "Channel") + .WithMany("LiquidityRules") + .HasForeignKey("ChannelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("NodeGuard.Data.Models.Node", "Node") + .WithMany() + .HasForeignKey("NodeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("NodeGuard.Data.Models.Wallet", "ReverseSwapWallet") + .WithMany("LiquidityRulesAsReverseSwapWallet") + .HasForeignKey("ReverseSwapWalletId"); + + b.HasOne("NodeGuard.Data.Models.Wallet", "SwapWallet") + .WithMany("LiquidityRulesAsSwapWallet") + .HasForeignKey("SwapWalletId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Channel"); + + b.Navigation("Node"); + + b.Navigation("ReverseSwapWallet"); + + b.Navigation("SwapWallet"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.Node", b => + { + b.HasOne("NodeGuard.Data.Models.Wallet", "ReturningFundsWallet") + .WithMany() + .HasForeignKey("ReturningFundsWalletId"); + + b.Navigation("ReturningFundsWallet"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.Wallet", b => + { + b.HasOne("NodeGuard.Data.Models.InternalWallet", "InternalWallet") + .WithMany() + .HasForeignKey("InternalWalletId"); + + b.Navigation("InternalWallet"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.WalletWithdrawalRequest", b => + { + b.HasOne("NodeGuard.Data.Models.ApplicationUser", "UserRequestor") + .WithMany("WalletWithdrawalRequests") + .HasForeignKey("UserRequestorId"); + + b.HasOne("NodeGuard.Data.Models.Wallet", "Wallet") + .WithMany() + .HasForeignKey("WalletId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("UserRequestor"); + + b.Navigation("Wallet"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.WalletWithdrawalRequestDestination", b => + { + b.HasOne("NodeGuard.Data.Models.WalletWithdrawalRequest", "WalletWithdrawalRequest") + .WithMany("WalletWithdrawalRequestDestinations") + .HasForeignKey("WalletWithdrawalRequestId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("WalletWithdrawalRequest"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.WalletWithdrawalRequestPSBT", b => + { + b.HasOne("NodeGuard.Data.Models.ApplicationUser", "Signer") + .WithMany() + .HasForeignKey("SignerId"); + + b.HasOne("NodeGuard.Data.Models.WalletWithdrawalRequest", "WalletWithdrawalRequest") + .WithMany("WalletWithdrawalRequestPSBTs") + .HasForeignKey("WalletWithdrawalRequestId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Signer"); + + b.Navigation("WalletWithdrawalRequest"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.Channel", b => + { + b.Navigation("ChannelOperationRequests"); + + b.Navigation("LiquidityRules"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.ChannelOperationRequest", b => + { + b.Navigation("ChannelOperationRequestPsbts"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.Node", b => + { + b.Navigation("ChannelOperationRequestsAsDestination"); + + b.Navigation("ChannelOperationRequestsAsSource"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.Wallet", b => + { + b.Navigation("ChannelOperationRequestsAsSource"); + + b.Navigation("LiquidityRulesAsReverseSwapWallet"); + + b.Navigation("LiquidityRulesAsSwapWallet"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.WalletWithdrawalRequest", b => + { + b.Navigation("WalletWithdrawalRequestDestinations"); + + b.Navigation("WalletWithdrawalRequestPSBTs"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.ApplicationUser", b => + { + b.Navigation("ChannelOperationRequests"); + + b.Navigation("Keys"); + + b.Navigation("WalletWithdrawalRequests"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Migrations/20250611154856_RemoveObsoleteWithdrawalProperties.cs b/src/Migrations/20250611154856_RemoveObsoleteWithdrawalProperties.cs new file mode 100644 index 00000000..bcf10764 --- /dev/null +++ b/src/Migrations/20250611154856_RemoveObsoleteWithdrawalProperties.cs @@ -0,0 +1,40 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace NodeGuard.Migrations +{ + /// + public partial class RemoveObsoleteWithdrawalProperties : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Amount", + table: "WalletWithdrawalRequests"); + + migrationBuilder.DropColumn( + name: "DestinationAddress", + table: "WalletWithdrawalRequests"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Amount", + table: "WalletWithdrawalRequests", + type: "numeric", + nullable: false, + defaultValue: 0m); + + migrationBuilder.AddColumn( + name: "DestinationAddress", + table: "WalletWithdrawalRequests", + type: "text", + nullable: false, + defaultValue: ""); + } + } +} From a92446ba546b8ebc6a10c79e64abe8f807e0d072 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20A=2EP?= <53834183+Jossec101@users.noreply.github.com> Date: Wed, 11 Jun 2025 18:02:08 +0200 Subject: [PATCH 04/16] Refactor wallet transfer logic to encapsulate destination address and amount in a list of withdrawal request destinations --- src/Pages/Wallets.razor | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Pages/Wallets.razor b/src/Pages/Wallets.razor index 36f50138..98f41881 100644 --- a/src/Pages/Wallets.razor +++ b/src/Pages/Wallets.razor @@ -1419,9 +1419,15 @@ OnSubmit="TransferFundsHotWallet"/> UserRequestorId = LoggedUser != null ? LoggedUser.Id : string.Empty, Description = $"Funds transferred from {_sourceWalletName} to {_targetWalletName}", WithdrawAllFunds = _transferAllFunds, - DestinationAddress = targetBitcoinAddress.ToString(), MempoolRecommendedFeesType = MempoolRecommendedFeesType.HourFee, - Amount = _amountToTransfer, + WalletWithdrawalRequestDestinations = new List + { + new WalletWithdrawalRequestDestination + { + Address = targetBitcoinAddress.ToString(), + Amount = _amountToTransfer, + } + }, WalletId = _sourceTransferWallet.Id, Status = _sourceTransferWallet.IsHotWallet ? WalletWithdrawalRequestStatus.PSBTSignaturesPending From 2d51d60bec1dafde4edd1131d2aaf6fed73eb773 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20A=2EP?= <53834183+Jossec101@users.noreply.github.com> Date: Wed, 11 Jun 2025 18:20:02 +0200 Subject: [PATCH 05/16] Update withdrawal request destination handling to use WalletWithdrawalRequestDestinations --- src/Pages/Withdrawals.razor | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Pages/Withdrawals.razor b/src/Pages/Withdrawals.razor index f50f9def..94146431 100644 --- a/src/Pages/Withdrawals.razor +++ b/src/Pages/Withdrawals.razor @@ -135,9 +135,9 @@ @ShowRequestor(context) - + - @context.DestinationAddress + @context.WalletWithdrawalRequestDestinations?.FirstOrDefault()?.Address @* TODO Copy and explorer buttons *@ @@ -322,9 +322,9 @@ @ShowRequestor(context) - + - @context.DestinationAddress + @context.WalletWithdrawalRequestDestinations?.FirstOrDefault()?.Address @* TODO Copy and explorer buttons *@ @@ -573,7 +573,6 @@ if (arg.Item == null) return; var amount = _selectedUTXOs.Count > 0 ? SelectedUTXOsValue() : _amount; arg.Item.Wallet = await WalletRepository.GetById(arg.Item.WalletId); - arg.Item.Amount = amount; arg.Item.Changeless = _selectedUTXOs.Count > 0; arg.Item.WithdrawAllFunds = _isCheckedAllFunds || _amount == _selectedRequestWalletBalance; arg.Item.MempoolRecommendedFeesType = _selectedMempoolRecommendedFeesType; From abd9a33325fbc25654ca051b93e0d3e042d6ff49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20A=2EP?= <53834183+Jossec101@users.noreply.github.com> Date: Wed, 11 Jun 2025 18:20:09 +0200 Subject: [PATCH 06/16] Refactor withdrawal request to use WalletWithdrawalRequestDestinations for improved structure --- src/Rpc/NodeGuardService.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Rpc/NodeGuardService.cs b/src/Rpc/NodeGuardService.cs index e95ea788..92c8d65b 100644 --- a/src/Rpc/NodeGuardService.cs +++ b/src/Rpc/NodeGuardService.cs @@ -221,8 +221,14 @@ public override async Task RequestWithdrawal(RequestW withdrawalRequest = new WalletWithdrawalRequest() { WalletId = request.WalletId, - Amount = amount, - DestinationAddress = request.Address, + WalletWithdrawalRequestDestinations = + { + new WalletWithdrawalRequestDestination() + { + Address = request.Address, + Amount = amount, + } + }, Description = request.Description, Status = wallet.IsHotWallet ? WalletWithdrawalRequestStatus.PSBTSignaturesPending From 73ee8f35a70b34409a46e7e461af4c3b3a70e548 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20A=2EP?= <53834183+Jossec101@users.noreply.github.com> Date: Wed, 11 Jun 2025 18:20:17 +0200 Subject: [PATCH 07/16] Enhance withdrawal request handling to ensure destination address is set and log errors for missing addresses --- src/Services/BitcoinService.cs | 37 ++++++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/src/Services/BitcoinService.cs b/src/Services/BitcoinService.cs index 8dbee02f..bff8230d 100644 --- a/src/Services/BitcoinService.cs +++ b/src/Services/BitcoinService.cs @@ -149,7 +149,17 @@ public async Task GenerateTemplatePSBT(WalletWithdrawalRequest walletWithd { var balanceResponse = await _nbXplorerService.GetBalanceAsync(derivationStrategy); - walletWithdrawalRequest.Amount = ((Money)balanceResponse.Confirmed).ToUnit(MoneyUnit.BTC); + var firstDestination = walletWithdrawalRequest.WalletWithdrawalRequestDestinations?.FirstOrDefault(); + if (firstDestination != null) + { + firstDestination.Amount = ((Money)balanceResponse.Confirmed).ToUnit(MoneyUnit.BTC); + } + else + { + _logger.LogError( + "Cannot set the amount for a full withdrawal of the wallet funds, no destination address was provided"); + throw new ArgumentException("No destination address was provided for the full withdrawal request"); + } var update = _walletWithdrawalRequestRepository.Update(walletWithdrawalRequest); if (!update.Item1) @@ -219,7 +229,12 @@ await _coinSelectionService.GetTxInputCoins(availableUTXOs, walletWithdrawalRequ var changelessAmount = selectedUTXOs.Sum(u => (Money)u.Value); var amount = new Money(walletWithdrawalRequest.SatsAmount, MoneyUnit.Satoshi); - var destination = BitcoinAddress.Create(walletWithdrawalRequest.DestinationAddress, nbXplorerNetwork); + var destinationAddress = walletWithdrawalRequest.WalletWithdrawalRequestDestinations?.FirstOrDefault()?.Address; + if (string.IsNullOrEmpty(destinationAddress)) + { + throw new ArgumentException("Destination address is null or empty."); + } + var destination = BitcoinAddress.Create(destinationAddress, nbXplorerNetwork); builder.SetSigningOptions(SigHash.All) .SetChange(changeAddress.Address) @@ -448,11 +463,21 @@ public async Task PerformWithdrawal(WalletWithdrawalRequest walletWithdrawalRequ } //We track the destination address - var trackedSourceAddress = TrackedSource.Create(BitcoinAddress.Create( - walletWithdrawalRequest.DestinationAddress, - CurrentNetworkHelper.GetCurrentNetwork())); + var destination = walletWithdrawalRequest.WalletWithdrawalRequestDestinations != null + ? walletWithdrawalRequest.WalletWithdrawalRequestDestinations.FirstOrDefault() + : null; + + if (destination?.Address != null) + { + var trackedSourceAddress = TrackedSource.Create( + BitcoinAddress.Create(destination.Address, CurrentNetworkHelper.GetCurrentNetwork())); - await _nbXplorerService.TrackAsync(trackedSourceAddress, new TrackWalletRequest { }, default); + await _nbXplorerService.TrackAsync(trackedSourceAddress, new TrackWalletRequest { }, default); + } + else + { + _logger.LogWarning("No valid destination address found to track for withdrawal request id: {RequestId}", walletWithdrawalRequest.Id); + } } catch (Exception e) { From 176edec34170fae0e784177d8f9d9cb633a24b02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20A=2EP?= <53834183+Jossec101@users.noreply.github.com> Date: Wed, 11 Jun 2025 18:26:56 +0200 Subject: [PATCH 08/16] Include WalletWithdrawalRequestDestinations in various retrieval methods for comprehensive request data --- src/Data/Repositories/WalletWithdrawalRequestRepository.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Data/Repositories/WalletWithdrawalRequestRepository.cs b/src/Data/Repositories/WalletWithdrawalRequestRepository.cs index 3ba90d5e..c9c78d94 100644 --- a/src/Data/Repositories/WalletWithdrawalRequestRepository.cs +++ b/src/Data/Repositories/WalletWithdrawalRequestRepository.cs @@ -64,6 +64,7 @@ INBXplorerService nBXplorerService .ThenInclude(x => x.Keys) .Include(x => x.UserRequestor) .Include(x => x.WalletWithdrawalRequestPSBTs) + .Include(x => x.WalletWithdrawalRequestDestinations) .SingleOrDefaultAsync(x => x.Id == id); return request; @@ -92,6 +93,7 @@ public async Task> GetAll() .Include(x => x.Wallet).ThenInclude(x => x.Keys) .Include(x => x.UserRequestor) .Include(x => x.WalletWithdrawalRequestPSBTs) + .Include(x => x.WalletWithdrawalRequestDestinations) .AsSplitQuery() .ToListAsync(); } @@ -104,6 +106,7 @@ public async Task> GetUnsignedPendingRequestsByUse .Include(x => x.Wallet).ThenInclude(x => x.Keys) .Include(x => x.UserRequestor) .Include(x => x.WalletWithdrawalRequestPSBTs) + .Include(x => x.WalletWithdrawalRequestDestinations) .Where(request => request.Wallet != null && request.Wallet.Keys.Count(key => userId == key.UserId) > request.WalletWithdrawalRequestPSBTs.Count(req => req.SignerId == userId) && (request.Status == WalletWithdrawalRequestStatus.Pending || request.Status == WalletWithdrawalRequestStatus.PSBTSignaturesPending)) @@ -119,6 +122,7 @@ public async Task> GetAllUnsignedPendingRequests() .Include(x => x.Wallet).ThenInclude(x => x.Keys) .Include(x => x.UserRequestor) .Include(x => x.WalletWithdrawalRequestPSBTs) + .Include(x => x.WalletWithdrawalRequestDestinations) .Where(request => request.Status == WalletWithdrawalRequestStatus.Pending || request.Status == WalletWithdrawalRequestStatus.PSBTSignaturesPending) .AsSplitQuery() .ToListAsync(); @@ -272,6 +276,7 @@ public async Task> GetPendingRequests() || request.Status == WalletWithdrawalRequestStatus.Pending || request.Status == WalletWithdrawalRequestStatus.PSBTSignaturesPending) .Include(request => request.Wallet) + .Include(x => x.WalletWithdrawalRequestDestinations) .ToListAsync(); return walletWithdrawalRequests; @@ -284,6 +289,7 @@ public async Task> GetOnChainPendingWithdrawals() var walletWithdrawalRequests = await applicationDbContext.WalletWithdrawalRequests .Where(request => request.Status == WalletWithdrawalRequestStatus.OnChainConfirmationPending) .Include(request => request.Wallet) + .Include(x => x.WalletWithdrawalRequestDestinations) .ToListAsync(); return walletWithdrawalRequests; From 7e127bbac7146399de7e8b4455e6be8721893af4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20A=2EP?= <53834183+Jossec101@users.noreply.github.com> Date: Wed, 11 Jun 2025 18:54:59 +0200 Subject: [PATCH 09/16] Refactor WalletWithdrawalRequest properties and update references to TotalAmount for consistency --- NodeGuard.code-workspace | 4 +++- src/Data/Models/WalletWithdrawalRequest.cs | 2 +- src/Data/Repositories/WalletWithdrawalRequestRepository.cs | 2 +- src/Shared/PSBTSign.razor | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/NodeGuard.code-workspace b/NodeGuard.code-workspace index 30d086ea..0d012286 100644 --- a/NodeGuard.code-workspace +++ b/NodeGuard.code-workspace @@ -7,5 +7,7 @@ "path": "src" } ], - "settings": {} + "settings": { + "editor.formatOnSave": false + } } \ No newline at end of file diff --git a/src/Data/Models/WalletWithdrawalRequest.cs b/src/Data/Models/WalletWithdrawalRequest.cs index c56b124a..cc085511 100644 --- a/src/Data/Models/WalletWithdrawalRequest.cs +++ b/src/Data/Models/WalletWithdrawalRequest.cs @@ -171,7 +171,7 @@ public bool Equals(WalletWithdrawalRequest? other) } [NotMapped] - public decimal Amount => + public decimal TotalAmount => WalletWithdrawalRequestDestinations?.Sum(x => x.Amount) ?? 0m; [NotMapped] diff --git a/src/Data/Repositories/WalletWithdrawalRequestRepository.cs b/src/Data/Repositories/WalletWithdrawalRequestRepository.cs index c9c78d94..a458bf92 100644 --- a/src/Data/Repositories/WalletWithdrawalRequestRepository.cs +++ b/src/Data/Repositories/WalletWithdrawalRequestRepository.cs @@ -159,7 +159,7 @@ public async Task> GetAllUnsignedPendingRequests() return (false, "Balance could not be retrieved from the wallet."); } - var requestMoneyAmount = new Money(type.Amount, MoneyUnit.BTC); + var requestMoneyAmount = new Money(type.TotalAmount, MoneyUnit.BTC); if ((Money)balance.Confirmed < requestMoneyAmount) { diff --git a/src/Shared/PSBTSign.razor b/src/Shared/PSBTSign.razor index 45cb4eb6..76dc927e 100644 --- a/src/Shared/PSBTSign.razor +++ b/src/Shared/PSBTSign.razor @@ -28,7 +28,7 @@
@ValidationErrors
- @(ChannelRequest?.Amount ?? WithdrawalRequest?.Amount) + @(ChannelRequest?.Amount ?? WithdrawalRequest?.TotalAmount) @GetRequestType() Or click on your nodeguard signing extension to automatically sign From c825620cb5a8379652d4ba8de3572863dfb9de3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20A=2EP?= <53834183+Jossec101@users.noreply.github.com> Date: Thu, 12 Jun 2025 11:45:55 +0200 Subject: [PATCH 10/16] Undo autofmt --- src/Data/DbInitializer.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Data/DbInitializer.cs b/src/Data/DbInitializer.cs index 1255e595..19d5e7b5 100644 --- a/src/Data/DbInitializer.cs +++ b/src/Data/DbInitializer.cs @@ -74,7 +74,7 @@ public static void Initialize(IServiceProvider serviceProvider) { applicationDbContext.Database.Migrate(); dataprotectionKeysContext.Database.Migrate(); - + isConnected = true; } catch (Exception e) @@ -328,7 +328,7 @@ public static void Initialize(IServiceProvider serviceProvider) var lastEventId = evts.LastEventId; logger?.LogInformation(lastEventId.ToString()); - + minerRPC.SendToAddress(legacyMultisigAddress, legacyMultisigFundCoins); minerRPC.SendToAddress(multisigAddress, multisigFundCoins); minerRPC.SendToAddress(singlesigAddress, singlesigFundCoins); @@ -336,7 +336,7 @@ public static void Initialize(IServiceProvider serviceProvider) //6 blocks to confirm minerRPC.Generate(6); - + var notification1 = WaitNbxplorerNotification(evts, legacyMultisigDerivationStrategy, lastEventId); var notification2 = WaitNbxplorerNotification(evts, multisigDerivationStrategy, lastEventId); var notification3 = WaitNbxplorerNotification(evts, singlesigDerivationStrategy, lastEventId); @@ -346,7 +346,7 @@ public static void Initialize(IServiceProvider serviceProvider) { throw new Exception("Wallets are not initialized"); } - + var legacyMultisigBalance = nbxplorerClient.GetBalance(legacyMultisigDerivationStrategy); var multisigBalance = nbxplorerClient.GetBalance(multisigDerivationStrategy); var singleSigbalance = nbxplorerClient.GetBalance(singlesigDerivationStrategy); @@ -377,7 +377,7 @@ public static void Initialize(IServiceProvider serviceProvider) applicationDbContext.Add(testingSinglesigWallet); applicationDbContext.Add(testingSingleSigBIP39Wallet); } - + // API Tokens generation for services var authenticatedServices = new Dictionary { @@ -386,10 +386,10 @@ public static void Initialize(IServiceProvider serviceProvider) { "Liquidator", "8rvSsUGeyXXdDQrHctcTey/xtHdZQEn945KHwccKp9Q=" }, { "ElenPay", "2otDr3IrdARnarQZU8RO0ImQko6CDICyLmGUflKUQWA=" } }; - + var existingTokens = applicationDbContext.ApiTokens.Where(token => authenticatedServices.Keys.Contains(token.Name)).ToList(); - + if (existingTokens.Count != authenticatedServices.Count && adminUser != null) { foreach (var service in authenticatedServices) @@ -463,7 +463,7 @@ private static APIToken CreateApiToken(string name, string token, string userId) IsBlocked = false, CreatorId = userId }; - + apiToken.SetCreationDatetime(); apiToken.SetUpdateDatetime(); From 5457307b027c07b632b4ab6ed956acc128657822 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20A=2EP?= <53834183+Jossec101@users.noreply.github.com> Date: Thu, 12 Jun 2025 14:09:11 +0200 Subject: [PATCH 11/16] Working single output withdrawals with new data model --- src/Pages/Withdrawals.razor | 50 ++++++++++++++++++++++++++++++------- 1 file changed, 41 insertions(+), 9 deletions(-) diff --git a/src/Pages/Withdrawals.razor b/src/Pages/Withdrawals.razor index 94146431..ba8ab530 100644 --- a/src/Pages/Withdrawals.razor +++ b/src/Pages/Withdrawals.razor @@ -135,11 +135,19 @@ @ShowRequestor(context)
- + - @context.WalletWithdrawalRequestDestinations?.FirstOrDefault()?.Address - @* TODO Copy and explorer buttons *@ + @(context.WalletWithdrawalRequestDestinations?.FirstOrDefault()?.Address ?? "No address provided") + + + + + + + + + @@ -154,10 +162,10 @@ } - + @{ - @($"{context.Amount:f8} BTC ({Math.Round(PriceConversionService.BtcToUsdConversion(context.Amount, _btcPrice), 2)} USD)") + @($"{context.TotalAmount:f8} BTC ({Math.Round(PriceConversionService.BtcToUsdConversion(context.TotalAmount, _btcPrice), 2)} USD)") } @@ -328,10 +336,10 @@ @* TODO Copy and explorer buttons *@ - + @{ - @($"{context.Amount:f8} BTC ({Math.Round(PriceConversionService.BtcToUsdConversion(context.Amount, _btcPrice), 2)} USD)") + @($"{context.TotalAmount:f8} BTC ({Math.Round(PriceConversionService.BtcToUsdConversion(context.TotalAmount, _btcPrice), 2)} USD)") } @@ -477,6 +485,7 @@ private bool _isCheckedAllFunds; private MempoolRecommendedFeesType _selectedMempoolRecommendedFeesType; private long _customSatPerVbAmount = 1; + private string _destinationAddress = string.Empty; public abstract class PendingWithdrawalsColumnName { @@ -577,6 +586,20 @@ arg.Item.WithdrawAllFunds = _isCheckedAllFunds || _amount == _selectedRequestWalletBalance; arg.Item.MempoolRecommendedFeesType = _selectedMempoolRecommendedFeesType; arg.Item.CustomFeeRate = _customSatPerVbAmount; + + // Set the destination address and amount from the UI properties + if (!string.IsNullOrEmpty(_destinationAddress)) + { + arg.Item.WalletWithdrawalRequestDestinations = new List + { + new WalletWithdrawalRequestDestination + { + Address = _destinationAddress, + Amount = amount + } + }; + } + if (arg.Item.Wallet.IsHotWallet) { await GetData(); @@ -694,10 +717,10 @@ { var balance = await BitcoinService.GetWalletConfirmedBalance(wallet); - if (arg.Item.Amount > balance.Item1) + if (arg.Item.TotalAmount > balance.Item1) { ToastService.ShowError( - $"The confirmed balance({balance.Item1:f8} BTC) for the selected wallet is less than the amount requested to withdraw ({arg.Item.Amount:f8} BTC)"); + $"The confirmed balance({balance.Item1:f8} BTC) for the selected wallet is less than the amount requested to withdraw ({arg.Item.TotalAmount:f8} BTC)"); await GetData(); return true; } @@ -729,6 +752,7 @@ { obj.UserRequestorId = LoggedUser.Id; _selectedRequestWalletBalance = null; + _destinationAddress = string.Empty; } } @@ -1062,6 +1086,14 @@ await OnMempoolFeeRateChange(_selectedMempoolRecommendedFeesType); _amount = _minimumWithdrawalAmount; + _destinationAddress = string.Empty; + } + + private void OnDestinationAddressChanged(CellEditContext context, string text) + { + _destinationAddress = text; + var amount = _selectedUTXOs.Count > 0 ? SelectedUTXOsValue() : _amount; + context.CellValue = new List { new() { Address = text, Amount = amount } }; } } From 1a6c4bc604648cc2f4d94e7a80d8197f0c88ee1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20A=2EP?= <53834183+Jossec101@users.noreply.github.com> Date: Thu, 12 Jun 2025 15:07:53 +0200 Subject: [PATCH 12/16] Fix tests to new withdrawal model --- src/Rpc/NodeGuardService.cs | 4 +- .../Data/Repositories/FUTXORepositoryTests.cs | 53 ++++++++-- .../Services/BitcoinServiceTests.cs | 100 ++++++++++++++---- 3 files changed, 126 insertions(+), 31 deletions(-) diff --git a/src/Rpc/NodeGuardService.cs b/src/Rpc/NodeGuardService.cs index 92c8d65b..10efdd96 100644 --- a/src/Rpc/NodeGuardService.cs +++ b/src/Rpc/NodeGuardService.cs @@ -222,13 +222,13 @@ public override async Task RequestWithdrawal(RequestW { WalletId = request.WalletId, WalletWithdrawalRequestDestinations = - { + [ new WalletWithdrawalRequestDestination() { Address = request.Address, Amount = amount, } - }, + ], Description = request.Description, Status = wallet.IsHotWallet ? WalletWithdrawalRequestStatus.PSBTSignaturesPending diff --git a/test/NodeGuard.Tests/Data/Repositories/FUTXORepositoryTests.cs b/test/NodeGuard.Tests/Data/Repositories/FUTXORepositoryTests.cs index d900e42c..844b2a05 100644 --- a/test/NodeGuard.Tests/Data/Repositories/FUTXORepositoryTests.cs +++ b/test/NodeGuard.Tests/Data/Repositories/FUTXORepositoryTests.cs @@ -24,15 +24,22 @@ private Mock> SetupDbContextFactory() public async Task GetLockedUTXOs_emptyArgs() { var dbContextFactory = SetupDbContextFactory(); - var futxoRepository = new FUTXORepository(null, null, dbContextFactory.Object); + var futxoRepository = new FUTXORepository(null!, null!, dbContextFactory.Object); var context = await dbContextFactory.Object.CreateDbContextAsync(); context.WalletWithdrawalRequests.Add(new WalletWithdrawalRequest { Description = "1", - DestinationAddress = "1", Status = WalletWithdrawalRequestStatus.Pending, + WalletWithdrawalRequestDestinations = new List + { + new WalletWithdrawalRequestDestination + { + Address = "1", + Amount = 0.01m + } + }, UTXOs = new List { new () { TxId = "1"} } }); context.ChannelOperationRequests.Add(new ChannelOperationRequest @@ -50,7 +57,7 @@ public async Task GetLockedUTXOs_emptyArgs() public async Task GetLockedUTXOs_ignoreWithdrawals() { var dbContextFactory = SetupDbContextFactory(); - var futxoRepository = new FUTXORepository(null, null, dbContextFactory.Object); + var futxoRepository = new FUTXORepository(null!, null!, dbContextFactory.Object); var context = dbContextFactory.Object.CreateDbContext(); @@ -58,8 +65,15 @@ public async Task GetLockedUTXOs_ignoreWithdrawals() { Id = 1, Description = "1", - DestinationAddress = "1", Status = WalletWithdrawalRequestStatus.Pending, + WalletWithdrawalRequestDestinations = new List + { + new WalletWithdrawalRequestDestination + { + Address = "1", + Amount = 0.01m + } + }, UTXOs = new List { new () { TxId = "1"} } }); context.ChannelOperationRequests.Add(new ChannelOperationRequest @@ -79,7 +93,7 @@ public async Task GetLockedUTXOs_ignoreWithdrawals() public async Task GetLockedUTXOs_ignoreChannels() { var dbContextFactory = SetupDbContextFactory(); - var futxoRepository = new FUTXORepository(null, null, dbContextFactory.Object); + var futxoRepository = new FUTXORepository(null!, null!, dbContextFactory.Object); var context = dbContextFactory.Object.CreateDbContext(); @@ -87,8 +101,15 @@ public async Task GetLockedUTXOs_ignoreChannels() { Id = 1, Description = "1", - DestinationAddress = "1", Status = WalletWithdrawalRequestStatus.Pending, + WalletWithdrawalRequestDestinations = new List + { + new WalletWithdrawalRequestDestination + { + Address = "1", + Amount = 0.01m + } + }, UTXOs = new List { new () { TxId = "1"} } }); context.ChannelOperationRequests.Add(new ChannelOperationRequest @@ -108,7 +129,7 @@ public async Task GetLockedUTXOs_ignoreChannels() public async Task GetLockedUTXOs_failedChannels() { var dbContextFactory = SetupDbContextFactory(); - var futxoRepository = new FUTXORepository(null, null, dbContextFactory.Object); + var futxoRepository = new FUTXORepository(null!, null!, dbContextFactory.Object); var context = dbContextFactory.Object.CreateDbContext(); @@ -116,8 +137,15 @@ public async Task GetLockedUTXOs_failedChannels() { Id = 1, Description = "1", - DestinationAddress = "1", Status = WalletWithdrawalRequestStatus.Failed, + WalletWithdrawalRequestDestinations = new List + { + new WalletWithdrawalRequestDestination + { + Address = "1", + Amount = 0.01m + } + }, UTXOs = new List { new () { TxId = "1"} } }); context.ChannelOperationRequests.Add(new ChannelOperationRequest @@ -145,8 +173,15 @@ public async Task GetLockedUTXOs_failedCWithdrawals() { Id = 1, Description = "1", - DestinationAddress = "1", Status = WalletWithdrawalRequestStatus.Pending, + WalletWithdrawalRequestDestinations = new List + { + new WalletWithdrawalRequestDestination + { + Address = "1", + Amount = 0.01m + } + }, UTXOs = new List { new () { TxId = "1"} } }); context.ChannelOperationRequests.Add(new ChannelOperationRequest diff --git a/test/NodeGuard.Tests/Services/BitcoinServiceTests.cs b/test/NodeGuard.Tests/Services/BitcoinServiceTests.cs index 9837c2c6..65e2afa1 100644 --- a/test/NodeGuard.Tests/Services/BitcoinServiceTests.cs +++ b/test/NodeGuard.Tests/Services/BitcoinServiceTests.cs @@ -139,8 +139,14 @@ async Task GenerateTemplatePSBT_LegacyMultiSigSucceeds() Status = WalletWithdrawalRequestStatus.Pending, Wallet = wallet, WalletWithdrawalRequestPSBTs = new List(), - Amount = 0.01m, - DestinationAddress = "bcrt1q8k3av6q5yp83rn332lx8a90k6kukhg28hs5qw7krdq95t629hgsqk6ztmf" + WalletWithdrawalRequestDestinations = new List + { + new WalletWithdrawalRequestDestination + { + Address = "bcrt1q8k3av6q5yp83rn332lx8a90k6kukhg28hs5qw7krdq95t629hgsqk6ztmf", + Amount = 0.01m + } + } }; var walletWithdrawalRequestRepository = new Mock(); @@ -211,8 +217,14 @@ async Task GenerateTemplatePSBT_MultiSigSucceeds() Status = WalletWithdrawalRequestStatus.Pending, Wallet = wallet, WalletWithdrawalRequestPSBTs = new List(), - Amount = 0.01m, - DestinationAddress = "bcrt1q8k3av6q5yp83rn332lx8a90k6kukhg28hs5qw7krdq95t629hgsqk6ztmf" + WalletWithdrawalRequestDestinations = new List + { + new WalletWithdrawalRequestDestination + { + Address = "bcrt1q8k3av6q5yp83rn332lx8a90k6kukhg28hs5qw7krdq95t629hgsqk6ztmf", + Amount = 0.01m + } + } }; var walletWithdrawalRequestRepository = new Mock(); @@ -284,8 +296,14 @@ async Task GenerateTemplatePSBT_SingleSigSucceeds() Status = WalletWithdrawalRequestStatus.Pending, Wallet = wallet, WalletWithdrawalRequestPSBTs = new List(), - Amount = 0.01m, - DestinationAddress = "bcrt1q8k3av6q5yp83rn332lx8a90k6kukhg28hs5qw7krdq95t629hgsqk6ztmf" + WalletWithdrawalRequestDestinations = new List + { + new WalletWithdrawalRequestDestination + { + Address = "bcrt1q8k3av6q5yp83rn332lx8a90k6kukhg28hs5qw7krdq95t629hgsqk6ztmf", + Amount = 0.01m + } + } }; var walletWithdrawalRequestRepository = new Mock(); @@ -356,8 +374,14 @@ async Task GenerateTemplatePSBT_SingleSigFailsFrozenUTXO() Status = WalletWithdrawalRequestStatus.Pending, Wallet = wallet, WalletWithdrawalRequestPSBTs = new List(), - Amount = 0.01m, - DestinationAddress = "bcrt1q8k3av6q5yp83rn332lx8a90k6kukhg28hs5qw7krdq95t629hgsqk6ztmf" + WalletWithdrawalRequestDestinations = new List + { + new WalletWithdrawalRequestDestination + { + Address = "bcrt1q8k3av6q5yp83rn332lx8a90k6kukhg28hs5qw7krdq95t629hgsqk6ztmf", + Amount = 0.01m + } + } }; var walletWithdrawalRequestRepository = new Mock(); @@ -441,8 +465,14 @@ async Task GenerateTemplatePSBT_SingleSigSuccessManuallyUnfrozenUTXO() Status = WalletWithdrawalRequestStatus.Pending, Wallet = wallet, WalletWithdrawalRequestPSBTs = new List(), - Amount = 0.01m, - DestinationAddress = "bcrt1q8k3av6q5yp83rn332lx8a90k6kukhg28hs5qw7krdq95t629hgsqk6ztmf" + WalletWithdrawalRequestDestinations = new List + { + new WalletWithdrawalRequestDestination + { + Address = "bcrt1q8k3av6q5yp83rn332lx8a90k6kukhg28hs5qw7krdq95t629hgsqk6ztmf", + Amount = 0.01m + } + } }; var walletWithdrawalRequestRepository = new Mock(); @@ -532,8 +562,14 @@ async Task GenerateTemplatePSBT_SingleSigFailsManuallyFrozenUTXO() Status = WalletWithdrawalRequestStatus.Pending, Wallet = wallet, WalletWithdrawalRequestPSBTs = new List(), - Amount = 0.01m, - DestinationAddress = "bcrt1q8k3av6q5yp83rn332lx8a90k6kukhg28hs5qw7krdq95t629hgsqk6ztmf" + WalletWithdrawalRequestDestinations = new List + { + new WalletWithdrawalRequestDestination + { + Address = "bcrt1q8k3av6q5yp83rn332lx8a90k6kukhg28hs5qw7krdq95t629hgsqk6ztmf", + Amount = 0.01m + } + } }; var walletWithdrawalRequestRepository = new Mock(); @@ -636,8 +672,14 @@ async Task GenerateTemplatePSBT_Changeless_SingleSigSucceeds() Status = WalletWithdrawalRequestStatus.Pending, Wallet = wallet, WalletWithdrawalRequestPSBTs = new List(), - Amount = 0.09m, - DestinationAddress = "bcrt1q8k3av6q5yp83rn332lx8a90k6kukhg28hs5qw7krdq95t629hgsqk6ztmf", + WalletWithdrawalRequestDestinations = new List + { + new WalletWithdrawalRequestDestination + { + Address = "bcrt1q8k3av6q5yp83rn332lx8a90k6kukhg28hs5qw7krdq95t629hgsqk6ztmf", + Amount = 0.09m + } + }, Changeless = true, }; @@ -718,8 +760,14 @@ async Task PerformWithdrawal_SingleSigSucceeds() Status = WalletWithdrawalRequestStatus.PSBTSignaturesPending, Wallet = wallet, WalletWithdrawalRequestPSBTs = walletWithdrawalRequestPSBTs, - Amount = 0.01m, - DestinationAddress = "bcrt1q8k3av6q5yp83rn332lx8a90k6kukhg28hs5qw7krdq95t629hgsqk6ztmf" + WalletWithdrawalRequestDestinations = new List + { + new WalletWithdrawalRequestDestination + { + Address = "bcrt1q8k3av6q5yp83rn332lx8a90k6kukhg28hs5qw7krdq95t629hgsqk6ztmf", + Amount = 0.01m + } + } }; var node = new Node() { @@ -798,8 +846,14 @@ async Task PerformWithdrawal_MultiSigSucceeds() Status = WalletWithdrawalRequestStatus.PSBTSignaturesPending, Wallet = wallet, WalletWithdrawalRequestPSBTs = walletWithdrawalRequestPSBTs, - Amount = 0.01m, - DestinationAddress = "bcrt1q8k3av6q5yp83rn332lx8a90k6kukhg28hs5qw7krdq95t629hgsqk6ztmf" + WalletWithdrawalRequestDestinations = new List + { + new WalletWithdrawalRequestDestination + { + Address = "bcrt1q8k3av6q5yp83rn332lx8a90k6kukhg28hs5qw7krdq95t629hgsqk6ztmf", + Amount = 0.01m + } + } }; var node = new Node() { @@ -878,8 +932,14 @@ async Task PerformWithdrawal_LegacyMultiSigSucceeds() Status = WalletWithdrawalRequestStatus.PSBTSignaturesPending, Wallet = wallet, WalletWithdrawalRequestPSBTs = walletWithdrawalRequestPSBTs, - Amount = 0.01m, - DestinationAddress = "bcrt1q8k3av6q5yp83rn332lx8a90k6kukhg28hs5qw7krdq95t629hgsqk6ztmf" + WalletWithdrawalRequestDestinations = new List + { + new WalletWithdrawalRequestDestination + { + Address = "bcrt1q8k3av6q5yp83rn332lx8a90k6kukhg28hs5qw7krdq95t629hgsqk6ztmf", + Amount = 0.01m + } + } }; var node = new Node() { From 268bcea14c04b699a66353b9ba45b006f2ab02c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20A=2EP?= <53834183+Jossec101@users.noreply.github.com> Date: Thu, 12 Jun 2025 15:15:19 +0200 Subject: [PATCH 13/16] Refactor destination address handling in WalletWithdrawalRequest to improve data binding and validation --- src/Pages/Withdrawals.razor | 32 ++++++++++++-------------------- 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/src/Pages/Withdrawals.razor b/src/Pages/Withdrawals.razor index ba8ab530..830e8edc 100644 --- a/src/Pages/Withdrawals.razor +++ b/src/Pages/Withdrawals.razor @@ -135,13 +135,13 @@ @ShowRequestor(context) - + @(context.WalletWithdrawalRequestDestinations?.FirstOrDefault()?.Address ?? "No address provided") - + @@ -330,7 +330,7 @@ @ShowRequestor(context) - + @context.WalletWithdrawalRequestDestinations?.FirstOrDefault()?.Address @* TODO Copy and explorer buttons *@ @@ -485,7 +485,6 @@ private bool _isCheckedAllFunds; private MempoolRecommendedFeesType _selectedMempoolRecommendedFeesType; private long _customSatPerVbAmount = 1; - private string _destinationAddress = string.Empty; public abstract class PendingWithdrawalsColumnName { @@ -587,19 +586,6 @@ arg.Item.MempoolRecommendedFeesType = _selectedMempoolRecommendedFeesType; arg.Item.CustomFeeRate = _customSatPerVbAmount; - // Set the destination address and amount from the UI properties - if (!string.IsNullOrEmpty(_destinationAddress)) - { - arg.Item.WalletWithdrawalRequestDestinations = new List - { - new WalletWithdrawalRequestDestination - { - Address = _destinationAddress, - Amount = amount - } - }; - } - if (arg.Item.Wallet.IsHotWallet) { await GetData(); @@ -752,7 +738,6 @@ { obj.UserRequestorId = LoggedUser.Id; _selectedRequestWalletBalance = null; - _destinationAddress = string.Empty; } } @@ -1086,14 +1071,21 @@ await OnMempoolFeeRateChange(_selectedMempoolRecommendedFeesType); _amount = _minimumWithdrawalAmount; - _destinationAddress = string.Empty; } private void OnDestinationAddressChanged(CellEditContext context, string text) { - _destinationAddress = text; var amount = _selectedUTXOs.Count > 0 ? SelectedUTXOsValue() : _amount; context.CellValue = new List { new() { Address = text, Amount = amount } }; } + private string GetDestinationAddressFromCellValue(object cellValue) + { + if (cellValue is List destinations && destinations.Any()) + { + return destinations.First().Address ?? string.Empty; + } + return string.Empty; + } + } From dddf7182ba9ae469dff2d33ded9ae5327a3f9c07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20A=2EP?= <53834183+Jossec101@users.noreply.github.com> Date: Fri, 13 Jun 2025 13:44:33 +0200 Subject: [PATCH 14/16] Remove redundant error logging in database migration exception handling --- src/Data/DbInitializer.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Data/DbInitializer.cs b/src/Data/DbInitializer.cs index 19d5e7b5..7995f966 100644 --- a/src/Data/DbInitializer.cs +++ b/src/Data/DbInitializer.cs @@ -79,7 +79,6 @@ public static void Initialize(IServiceProvider serviceProvider) } catch (Exception e) { - logger.LogError(e, "Error while migrating"); throw new Exception("Error while migrating the database", e); } From 6d360f565df6eb9c1b16057fb6be08a45f612d7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20A=2EP?= <53834183+Jossec101@users.noreply.github.com> Date: Fri, 13 Jun 2025 16:59:02 +0200 Subject: [PATCH 15/16] Update src/Services/BitcoinService.cs Co-authored-by: Rodrigo <39995243+RodriFS@users.noreply.github.com> --- src/Services/BitcoinService.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Services/BitcoinService.cs b/src/Services/BitcoinService.cs index bff8230d..31455521 100644 --- a/src/Services/BitcoinService.cs +++ b/src/Services/BitcoinService.cs @@ -463,9 +463,7 @@ public async Task PerformWithdrawal(WalletWithdrawalRequest walletWithdrawalRequ } //We track the destination address - var destination = walletWithdrawalRequest.WalletWithdrawalRequestDestinations != null - ? walletWithdrawalRequest.WalletWithdrawalRequestDestinations.FirstOrDefault() - : null; + var destination = walletWithdrawalRequest.WalletWithdrawalRequestDestinations?.FirstOrDefault(); if (destination?.Address != null) { From 698bf82a90be934d6458fec0ab60439fe275cdf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20A=2EP?= <53834183+Jossec101@users.noreply.github.com> Date: Fri, 13 Jun 2025 17:02:13 +0200 Subject: [PATCH 16/16] Remove rollback logic for migrated destinations in Down migration method --- ...50611142943_MigrateLegacyWithdrawalData.cs | 21 ------------------- 1 file changed, 21 deletions(-) diff --git a/src/Migrations/20250611142943_MigrateLegacyWithdrawalData.cs b/src/Migrations/20250611142943_MigrateLegacyWithdrawalData.cs index b31497f9..9346fd96 100644 --- a/src/Migrations/20250611142943_MigrateLegacyWithdrawalData.cs +++ b/src/Migrations/20250611142943_MigrateLegacyWithdrawalData.cs @@ -58,27 +58,6 @@ SELECT COUNT(*) INTO migrated_count /// protected override void Down(MigrationBuilder migrationBuilder) { - // Rollback: Delete the migrated destinations in an explicit transaction - migrationBuilder.Sql(@" - BEGIN; - - -- Delete only the migrated destinations (those that match legacy withdrawal requests) - DELETE FROM ""WalletWithdrawalRequestDestinations"" - WHERE ""WalletWithdrawalRequestId"" IN ( - SELECT ""Id"" FROM ""WalletWithdrawalRequests"" - WHERE ""Amount"" > 0 AND ""DestinationAddress"" IS NOT NULL AND ""DestinationAddress"" != '' - ) - AND ""Address"" IN ( - SELECT ""DestinationAddress"" FROM ""WalletWithdrawalRequests"" - WHERE ""Amount"" > 0 AND ""DestinationAddress"" IS NOT NULL AND ""DestinationAddress"" != '' - ) - AND ""Amount"" IN ( - SELECT ""Amount"" FROM ""WalletWithdrawalRequests"" - WHERE ""Amount"" > 0 AND ""DestinationAddress"" IS NOT NULL AND ""DestinationAddress"" != '' - ); - - COMMIT; - "); } } }