From cfa14e7450122cf15d46bdbd1e1aa0c17f4150aa Mon Sep 17 00:00:00 2001 From: Marcos Date: Fri, 23 May 2025 16:02:23 +0200 Subject: [PATCH 01/49] fix: connection problems, healthcheck postgres --- .vscode/launch.json | 2 +- docker/docker-compose.yml | 27 +++++++++++++++++---------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 58374344..1c0e8945 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -21,7 +21,7 @@ }, "env": { "ASPNETCORE_ENVIRONMENT": "Development", - "POSTGRES_CONNECTIONSTRING": "Host=127.0.0.1;Port=5432;Database=nodeguard;Username=rw_dev;Password=rw_dev", + "POSTGRES_CONNECTIONSTRING": "Host=127.0.0.1;Port=25432;Database=nodeguard;Username=rw_dev;Password=rw_dev", "BITCOIN_NETWORK": "REGTEST", "MAXIMUM_WITHDRAWAL_BTC_AMOUNT": "21000000", "NBXPLORER_ENABLE_CUSTOM_BACKEND": "true", diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 5bb4af40..13021b8a 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -1,4 +1,4 @@ -version: '3.4' +version: "3.4" name: nodeguard services: @@ -22,12 +22,16 @@ services: platform: linux/amd64 hostname: nbxplorer ports: - - "32838:32838" + - "32838:32838" + depends_on: + - nbxplorer_postgres environment: NBXPLORER_NETWORK: regtest NBXPLORER_BIND: 0.0.0.0:32838 NBXPLORER_TRIMEVENTS: 10000 NBXPLORER_SIGNALFILESDIR: /datadir + #Keeping dbtrie for dev until it is fully removed since we would need to modify nbxplorer docker image to wait for the db to be ready + NBXPLORER_DBTRIE: 0 NBXPLORER_POSTGRES: Host=nbxplorer_postgres;Port=5432;Database=nbxplorer;Username=rw_dev;Password=rw_dev NBXPLORER_CHAINS: "btc" NBXPLORER_BTCRPCUSER: "polaruser" @@ -36,7 +40,7 @@ services: NBXPLORER_BTCNODEENDPOINT: host.docker.internal:19444 command: ["--noauth"] volumes: - - "bitcoin_datadir:/root/.bitcoin" + - "bitcoin_datadir:/root/.bitcoin" nbxplorer_postgres: container_name: nbxplorer_postgres @@ -51,12 +55,15 @@ services: - nbxplorer_postgres_data:/var/lib/postgresql/data ports: - 35432:5432 - - + healthcheck: + test: ["CMD", "pg_isready", "-U", "rw_dev"] + interval: 10s + timeout: 5s + retries: 5 volumes: - nodeguard_postgres_data: - bitcoin_datadir: - nbxplorer_datadir: - nbxplorer_postgres_data: - nodeguard_data_keys_dir: + nodeguard_postgres_data: + bitcoin_datadir: + nbxplorer_datadir: + nbxplorer_postgres_data: + nodeguard_data_keys_dir: From 79fa4c8ba36b683cbc6c8a76c8e7d7e116dbc48f Mon Sep 17 00:00:00 2001 From: Anon2148 Date: Wed, 4 Jun 2025 00:17:58 +0200 Subject: [PATCH 02/49] feat: added ui and minimal functionality to display bumpfee pop up (not working though) --- src/Pages/Withdrawals.razor | 24 ++++++ src/Shared/BumpfeeModal.razor | 147 ++++++++++++++++++++++++++++++++++ 2 files changed, 171 insertions(+) create mode 100644 src/Shared/BumpfeeModal.razor diff --git a/src/Pages/Withdrawals.razor b/src/Pages/Withdrawals.razor index e70a47ac..479f8c33 100644 --- a/src/Pages/Withdrawals.razor +++ b/src/Pages/Withdrawals.razor @@ -58,6 +58,17 @@ + + @{ + var canBumpfee = context.Status == WalletWithdrawalRequestStatus.OnChainConfirmationPending; + var isRequestor = LoggedUser?.Id == context.UserRequestorId; + } + + @if (canBumpfee && isRequestor) + { + + } + @@ -408,6 +419,11 @@ TemplatePsbtString="@_templatePsbtString" ApproveRequestDelegate="async () => await ApproveRequestDelegate()"/> + + _selectedUTXOs = new(); private WalletWithdrawalRequest? _selectedRequest; private PSBTSign? _psbtSignRef; + private BumpfeeModal _bumpfeeRef; private string _signedPSBT; private string? _templatePsbtString; private int? _selectedWalletId; @@ -756,6 +773,13 @@ } } + private async Task ShowBumpfeeModal(WalletWithdrawalRequest walletWithdrawalRequest) + { + _selectedRequest = walletWithdrawalRequest; + + await _bumpfeeRef.ShowModal(); + } + private async Task Approve() { if (_selectedRequest == null || string.IsNullOrEmpty(_psbtSignRef?.SignedPSBT) || LoggedUser == null) diff --git a/src/Shared/BumpfeeModal.razor b/src/Shared/BumpfeeModal.razor new file mode 100644 index 00000000..d736c022 --- /dev/null +++ b/src/Shared/BumpfeeModal.razor @@ -0,0 +1,147 @@ +@using NBitcoin +@using Microsoft.CodeAnalysis.Text +@using System.Xml.Schema +@using JSException = Microsoft.JSInterop.JSException +@using System.Runtime.InteropServices.JavaScript +@inject IJSRuntime JS + + + + Bumpfee tx: @(ChannelRequest?.Id ?? WithdrawalRequest?.Id ?? 0) + + + + + + @nameof(WalletWithdrawalRequest.Description) + + + + @nameof(WalletWithdrawalRequest.WalletId) + + + + @nameof(WalletWithdrawalRequest.DestinationAddress) + + + + @nameof(WalletWithdrawalRequest.WithdrawAllFunds) + + + + Wallet Balance + @if (_selectedWalletBalance != null) + { +

@($"{_selectedWalletBalance:f8} BTC ({Math.Round(PriceConversionService.BtcToUsdConversion((decimal) _selectedWalletBalance, _btcPrice), 2)} USD)")

+ } +
+ + @nameof(WalletWithdrawalRequest.Amount) + + + @{ + var amountToShow = _amount < Constants.MAXIMUM_WITHDRAWAL_BTC_AMOUNT + ? _amount + : Constants.MAXIMUM_WITHDRAWAL_BTC_AMOUNT; + var convertedAmount = Math.Round(PriceConversionService.SatToUsdConversion(new Money(amountToShow, MoneyUnit.BTC).Satoshi, _btcPrice), 2); + } + @($"Minimum {_minimumWithdrawalAmount:f8} BTC. Current amount: {convertedAmount} USD") + +
+ or use + +
+
+ + @nameof(WalletWithdrawalRequest.CustomFeeRate) +
+ + + +
+ @(_selectedMempoolRecommendedFeesType != MempoolRecommendedFeesType.CustomFee ? "Fees may change by the time the request is first signed" : "") +
+
+
+ + + + +
+
+ +@inject IJSRuntime JsRuntime +@inject IBitcoinService BitcoinService +@inject IPriceConversionService PriceConversionService +@inject INBXplorerService NBXplorerService +@inject IWalletRepository WalletRepository +@code { + [Inject] + public IToastService ToastService { get; set; } + + [Parameter] + public ChannelOperationRequest? ChannelRequest { get; set; } + [Parameter] + public WalletWithdrawalRequest? WithdrawalRequest { get; set; } + + [Parameter] + public Action ApproveBumpfeeRequest { get; set; } + + private decimal? _selectedWalletBalance; + private decimal _btcPrice; + + private static readonly decimal _minimumWithdrawalAmount = Constants.MINIMUM_WITHDRAWAL_BTC_AMOUNT; + + private decimal _amount { get; set; } = _minimumWithdrawalAmount; + + private MempoolRecommendedFeesType _selectedMempoolRecommendedFeesType; + private long _customSatPerVbAmount = 1; + + private Modal? _modalRef; + + private async Task HandleOnClick() + { + await HideModal(); + } + + public async Task ShowModal() + { + ValidationErrors = string.Empty; + await JsRuntime.InvokeAsync("clearText"); + var walletId = WithdrawalRequest?.WalletId ?? -1; + if (walletId != -1) { + var wallet = await WalletRepository.GetById(walletId); + var (balance, _) = await BitcoinService.GetWalletConfirmedBalance(wallet); + _selectedWalletBalance = balance; + } + _btcPrice = await PriceConversionService.GetBtcToUsdPrice(); + if (_modalRef != null) + await _modalRef.Show(); + } + + public async Task HideModal() + { + if (_modalRef != null) + await _modalRef.Close(CloseReason.UserClosing); + } + + private async Task OnMempoolFeeRateChange(MempoolRecommendedFeesType value) + { + _selectedMempoolRecommendedFeesType = value; + _customSatPerVbAmount = (long?)await NBXplorerService.GetFeesByType(_selectedMempoolRecommendedFeesType) ?? 1; + } +} From 15c8f2e4d6d17e76f4b2b4825b4b975cd0a417f1 Mon Sep 17 00:00:00 2001 From: Anon2148 Date: Wed, 4 Jun 2025 00:17:58 +0200 Subject: [PATCH 03/49] feat: added actions column in withdrawal requests datagrid and functionality to display bumpfee button (not sure if it works) --- src/Pages/Withdrawals.razor | 43 ++++++++++++++++++++++++----------- src/Shared/BumpfeeModal.razor | 11 +++------ 2 files changed, 33 insertions(+), 21 deletions(-) diff --git a/src/Pages/Withdrawals.razor b/src/Pages/Withdrawals.razor index 479f8c33..20b4128b 100644 --- a/src/Pages/Withdrawals.razor +++ b/src/Pages/Withdrawals.razor @@ -58,17 +58,6 @@ - - @{ - var canBumpfee = context.Status == WalletWithdrawalRequestStatus.OnChainConfirmationPending; - var isRequestor = LoggedUser?.Id == context.UserRequestorId; - } - - @if (canBumpfee && isRequestor) - { - - } -
@@ -389,6 +378,20 @@ } + + + @if (_isNodeManager && ShowActionDropdown(context).Result) + { + + + + + Bumpfee + + + } + + @@ -421,8 +424,7 @@ + WithdrawalRequest="_selectedRequest"/> ShowActionDropdown(WalletWithdrawalRequest walletWithdrawalRequest) + { + if (!string.IsNullOrEmpty(walletWithdrawalRequest.TxId)) { + try { + var getTxResult = await NBXplorerService.GetTransactionAsync(uint256.Parse(walletWithdrawalRequest.TxId), default); + return walletWithdrawalRequest.Status == WalletWithdrawalRequestStatus.OnChainConfirmationPending && getTxResult.Confirmations == long.Parse("0"); + }catch (Exception e) + { + throw new Exception(e.Message); + } + } + + return string.IsNullOrEmpty(walletWithdrawalRequest.TxId); + } + private void OnColumnLayoutUpdate() { StateHasChanged(); diff --git a/src/Shared/BumpfeeModal.razor b/src/Shared/BumpfeeModal.razor index d736c022..30dde2e4 100644 --- a/src/Shared/BumpfeeModal.razor +++ b/src/Shared/BumpfeeModal.razor @@ -14,7 +14,7 @@ @nameof(WalletWithdrawalRequest.Description) - + @(WithdrawalRequest?.Description ?? String.Empty) @nameof(WalletWithdrawalRequest.WalletId) @@ -30,7 +30,7 @@ @nameof(WalletWithdrawalRequest.WithdrawAllFunds) - + Wallet Balance @@ -41,7 +41,7 @@ @nameof(WalletWithdrawalRequest.Amount) - + @{ var amountToShow = _amount < Constants.MAXIMUM_WITHDRAWAL_BTC_AMOUNT @@ -98,9 +98,6 @@ [Parameter] public WalletWithdrawalRequest? WithdrawalRequest { get; set; } - [Parameter] - public Action ApproveBumpfeeRequest { get; set; } - private decimal? _selectedWalletBalance; private decimal _btcPrice; @@ -120,8 +117,6 @@ public async Task ShowModal() { - ValidationErrors = string.Empty; - await JsRuntime.InvokeAsync("clearText"); var walletId = WithdrawalRequest?.WalletId ?? -1; if (walletId != -1) { var wallet = await WalletRepository.GetById(walletId); From e623755e8f737fb82e0d4513a6850561ded12df8 Mon Sep 17 00:00:00 2001 From: Anon2148 Date: Mon, 9 Jun 2025 13:35:44 +0200 Subject: [PATCH 04/49] fix: changed bumpefee modal rendering condition, although not tested --- src/Pages/Withdrawals.razor | 7 +++++-- src/Shared/BumpfeeModal.razor | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/Pages/Withdrawals.razor b/src/Pages/Withdrawals.razor index 20b4128b..01ae524d 100644 --- a/src/Pages/Withdrawals.razor +++ b/src/Pages/Withdrawals.razor @@ -380,7 +380,7 @@ - @if (_isNodeManager && ShowActionDropdown(context).Result) + @if (_isFinanceManager && ShowActionDropdown(context).Result) { @@ -1063,7 +1063,10 @@ if (!string.IsNullOrEmpty(walletWithdrawalRequest.TxId)) { try { var getTxResult = await NBXplorerService.GetTransactionAsync(uint256.Parse(walletWithdrawalRequest.TxId), default); - return walletWithdrawalRequest.Status == WalletWithdrawalRequestStatus.OnChainConfirmationPending && getTxResult.Confirmations == long.Parse("0"); + var showCondition = walletWithdrawalRequest.Status == WalletWithdrawalRequestStatus.OnChainConfirmationPending && getTxResult.Confirmations == (int) 0; + if (showCondition == false) { + ToastService.ShowError("Cannot show dropdown"); + } }catch (Exception e) { throw new Exception(e.Message); diff --git a/src/Shared/BumpfeeModal.razor b/src/Shared/BumpfeeModal.razor index 30dde2e4..95c38f9f 100644 --- a/src/Shared/BumpfeeModal.razor +++ b/src/Shared/BumpfeeModal.razor @@ -4,7 +4,9 @@ @using JSException = Microsoft.JSInterop.JSException @using System.Runtime.InteropServices.JavaScript @inject IJSRuntime JS - +@if (WithdrawalRequest != null) +{ + Bumpfee tx: @(ChannelRequest?.Id ?? WithdrawalRequest?.Id ?? 0) @@ -82,7 +84,8 @@ - + +} @inject IJSRuntime JsRuntime @inject IBitcoinService BitcoinService From 2933be8a0c7b59d796cc6fc11bad27c9b159bf79 Mon Sep 17 00:00:00 2001 From: Anon2148 Date: Mon, 9 Jun 2025 16:18:45 +0200 Subject: [PATCH 05/49] wip --- src/Pages/Withdrawals.razor | 67 ++++++++++++++++++++++++----------- src/Shared/BumpfeeModal.razor | 54 +++------------------------- 2 files changed, 52 insertions(+), 69 deletions(-) diff --git a/src/Pages/Withdrawals.razor b/src/Pages/Withdrawals.razor index 01ae524d..6fa83c54 100644 --- a/src/Pages/Withdrawals.razor +++ b/src/Pages/Withdrawals.razor @@ -380,15 +380,15 @@ - @if (_isFinanceManager && ShowActionDropdown(context).Result) + @if (_isFinanceManager && context.Status == WalletWithdrawalRequestStatus.OnChainConfirmationPending) { - - - - - Bumpfee - - + + + + + Bump fee + + } @@ -472,7 +472,7 @@ private List _selectedUTXOs = new(); private WalletWithdrawalRequest? _selectedRequest; private PSBTSign? _psbtSignRef; - private BumpfeeModal _bumpfeeRef; + private BumpfeeModal? _bumpfeeRef; private string _signedPSBT; private string? _templatePsbtString; private int? _selectedWalletId; @@ -776,10 +776,37 @@ } private async Task ShowBumpfeeModal(WalletWithdrawalRequest walletWithdrawalRequest) - { - _selectedRequest = walletWithdrawalRequest; + { + ulong confirmations = 0; + if (!string.IsNullOrEmpty(walletWithdrawalRequest.TxId)) { + try + { + var nbxplorerStatus = await NBXplorerService.GetTransactionAsync(uint256.Parse(walletWithdrawalRequest.TxId)); + confirmations = (ulong)(nbxplorerStatus?.Confirmations ?? 0); + } + catch (Exception) + { + ToastService.ShowError("Error getting confirmations"); + return; + } + } + + if (confirmations > 0) + { + ToastService.ShowError("Bumpfee can only be used for transactions with no confirmations. " + + "This transaction has already been mined."); + return; + } + + try { + _selectedRequest = walletWithdrawalRequest; - await _bumpfeeRef.ShowModal(); + if (_bumpfeeRef != null) await _bumpfeeRef.ShowModal(); + } + catch (Exception) + { + ToastService.ShowError("Error while creating bumpfee modal"); + } } private async Task Approve() @@ -1060,20 +1087,20 @@ private async Task ShowActionDropdown(WalletWithdrawalRequest walletWithdrawalRequest) { + ulong confirmations = 0; if (!string.IsNullOrEmpty(walletWithdrawalRequest.TxId)) { try { - var getTxResult = await NBXplorerService.GetTransactionAsync(uint256.Parse(walletWithdrawalRequest.TxId), default); - var showCondition = walletWithdrawalRequest.Status == WalletWithdrawalRequestStatus.OnChainConfirmationPending && getTxResult.Confirmations == (int) 0; - if (showCondition == false) { - ToastService.ShowError("Cannot show dropdown"); - } - }catch (Exception e) + var nbxplorerStatus = await NBXplorerService.GetTransactionAsync(uint256.Parse(walletWithdrawalRequest.TxId)); + confirmations = (ulong)(nbxplorerStatus?.Confirmations ?? 0); + return walletWithdrawalRequest.Status == WalletWithdrawalRequestStatus.OnChainConfirmationPending && confirmations.Equals((ulong) 0); + } + catch (Exception) { - throw new Exception(e.Message); + ToastService.ShowError("Error while showing bumpfee action"); } } - return string.IsNullOrEmpty(walletWithdrawalRequest.TxId); + return false; } private void OnColumnLayoutUpdate() diff --git a/src/Shared/BumpfeeModal.razor b/src/Shared/BumpfeeModal.razor index 95c38f9f..0b4c21af 100644 --- a/src/Shared/BumpfeeModal.razor +++ b/src/Shared/BumpfeeModal.razor @@ -4,9 +4,9 @@ @using JSException = Microsoft.JSInterop.JSException @using System.Runtime.InteropServices.JavaScript @inject IJSRuntime JS -@if (WithdrawalRequest != null) -{ - + + @if (WithdrawalRequest != null) + { Bumpfee tx: @(ChannelRequest?.Id ?? WithdrawalRequest?.Id ?? 0) @@ -14,50 +14,6 @@ - - @nameof(WalletWithdrawalRequest.Description) - @(WithdrawalRequest?.Description ?? String.Empty) - - - @nameof(WalletWithdrawalRequest.WalletId) - - - - @nameof(WalletWithdrawalRequest.DestinationAddress) - - - - @nameof(WalletWithdrawalRequest.WithdrawAllFunds) - - - - Wallet Balance - @if (_selectedWalletBalance != null) - { -

@($"{_selectedWalletBalance:f8} BTC ({Math.Round(PriceConversionService.BtcToUsdConversion((decimal) _selectedWalletBalance, _btcPrice), 2)} USD)")

- } -
- - @nameof(WalletWithdrawalRequest.Amount) - - - @{ - var amountToShow = _amount < Constants.MAXIMUM_WITHDRAWAL_BTC_AMOUNT - ? _amount - : Constants.MAXIMUM_WITHDRAWAL_BTC_AMOUNT; - var convertedAmount = Math.Round(PriceConversionService.SatToUsdConversion(new Money(amountToShow, MoneyUnit.BTC).Satoshi, _btcPrice), 2); - } - @($"Minimum {_minimumWithdrawalAmount:f8} BTC. Current amount: {convertedAmount} USD") - -
- or use - -
-
@nameof(WalletWithdrawalRequest.CustomFeeRate)
@@ -84,8 +40,8 @@ - -} + } + @inject IJSRuntime JsRuntime @inject IBitcoinService BitcoinService From 60adbf7dc5fa32188cf28e6b3a88fb10b6739f97 Mon Sep 17 00:00:00 2001 From: Anon2148 Date: Tue, 10 Jun 2025 13:37:14 +0200 Subject: [PATCH 06/49] feat: added fee rate check, user cannot bumpfee with a lower new fee rate than the current one --- src/Shared/BumpfeeModal.razor | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Shared/BumpfeeModal.razor b/src/Shared/BumpfeeModal.razor index 0b4c21af..3f631ae8 100644 --- a/src/Shared/BumpfeeModal.razor +++ b/src/Shared/BumpfeeModal.razor @@ -71,6 +71,11 @@ private async Task HandleOnClick() { + + long _currentSatsPerVbAmount = (long?)await NBXplorerService.GetFeesByType(WithdrawalRequest.MempoolRecommendedFeesType) ?? 1; + if (_customSatPerVbAmount <= _currentSatsPerVbAmount) { + ToastService.ShowError("Fee must be greater than the current one"); + } await HideModal(); } From 67bc76bc5e4363133891ad0deba8cd48f0e6aaa3 Mon Sep 17 00:00:00 2001 From: Anon2148 Date: Thu, 12 Jun 2025 18:48:24 +0200 Subject: [PATCH 07/49] wip: submit confirmation modal fails when trying to bumpfee --- src/Pages/Withdrawals.razor | 65 ++++++++++++++++++++++++++++++++++- src/Shared/BumpfeeModal.razor | 8 ++++- 2 files changed, 71 insertions(+), 2 deletions(-) diff --git a/src/Pages/Withdrawals.razor b/src/Pages/Withdrawals.razor index 6fa83c54..b5d6470c 100644 --- a/src/Pages/Withdrawals.razor +++ b/src/Pages/Withdrawals.razor @@ -424,7 +424,8 @@ + WithdrawalRequest="_selectedRequest" + SubmitBumpfeeModal="@SubmitBumpfeeModal"/> 0) + { + await CoinSelectionService.LockUTXOs(_selectedUTXOs, newRequest, BitcoinRequestType.WalletWithdrawal); + } + + _utxoSelectorModalRef.ClearModal(); + } + else + { + ToastService.ShowError("Something went wrong"); + _userPendingRequests.Remove(newRequest); + } + } + + private async Task Bumpfee(WalletWithdrawalRequest request) + { + try + { + await CreateNewWithdrawal(request); + StateHasChanged(); + } catch { + ToastService.ShowError("Something went wrong"); + } + } + + private async void SubmitBumpfeeModal() + { + if (_bumpfeeRef != null && _bumpfeeRef.WithdrawalRequest != null) + { + _selectedMempoolRecommendedFeesType = _bumpfeeRef.WithdrawalRequest.MempoolRecommendedFeesType; + if (_bumpfeeRef.WithdrawalRequest.CustomFeeRate != null) + _customSatPerVbAmount = (long)_bumpfeeRef.WithdrawalRequest.CustomFeeRate; + if (_selectedRequest != null) { + await Bumpfee(_selectedRequest); + } else { + ToastService.ShowError("Error while getting the withdrawal"); + } + await _bumpfeeRef.HideModal(); + } else { + ToastService.ShowError("Something went wrong"); + } + } + private async Task ApproveRequestDelegate() { if (_selectedRequest != null) diff --git a/src/Shared/BumpfeeModal.razor b/src/Shared/BumpfeeModal.razor index 3f631ae8..3a6ba242 100644 --- a/src/Shared/BumpfeeModal.razor +++ b/src/Shared/BumpfeeModal.razor @@ -57,6 +57,9 @@ [Parameter] public WalletWithdrawalRequest? WithdrawalRequest { get; set; } + [Parameter] + public Action SubmitBumpfeeModal { get; set; } + private decimal? _selectedWalletBalance; private decimal _btcPrice; @@ -75,8 +78,11 @@ long _currentSatsPerVbAmount = (long?)await NBXplorerService.GetFeesByType(WithdrawalRequest.MempoolRecommendedFeesType) ?? 1; if (_customSatPerVbAmount <= _currentSatsPerVbAmount) { ToastService.ShowError("Fee must be greater than the current one"); + } else { + WithdrawalRequest.MempoolRecommendedFeesType = _selectedMempoolRecommendedFeesType; + WithdrawalRequest.CustomFeeRate = _customSatPerVbAmount; + SubmitBumpfeeModal?.Invoke(); } - await HideModal(); } public async Task ShowModal() From fb9f324ac378b1d9b5ecbec893812a5498cccb80 Mon Sep 17 00:00:00 2001 From: Rubenro01 Date: Fri, 13 Jun 2025 22:03:07 +0200 Subject: [PATCH 08/49] feat: enhance fee rate display in withdrawal requests datagrid to clarify custom fee and value usage --- src/Pages/Withdrawals.razor | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Pages/Withdrawals.razor b/src/Pages/Withdrawals.razor index b5d6470c..f37c1c43 100644 --- a/src/Pages/Withdrawals.razor +++ b/src/Pages/Withdrawals.razor @@ -193,7 +193,7 @@ - @(context.MempoolRecommendedFeesType == MempoolRecommendedFeesType.CustomFee ? $"{context.CustomFeeRate} sats/vb" : context.MempoolRecommendedFeesType.Humanize()) + @(context.MempoolRecommendedFeesType == MempoolRecommendedFeesType.CustomFee ? $"Custom fee ({context.CustomFeeRate} sats/vb)" : $"{context.MempoolRecommendedFeesType.Humanize()} ({context.CustomFeeRate} sats/vb)")
@@ -338,7 +338,7 @@ - @(context.MempoolRecommendedFeesType == MempoolRecommendedFeesType.CustomFee ? $"{context.CustomFeeRate} sats/vb" : context.MempoolRecommendedFeesType.Humanize()) + @(context.MempoolRecommendedFeesType == MempoolRecommendedFeesType.CustomFee ? $"Custom fee ({context.CustomFeeRate} sats/vb)" : $"{context.MempoolRecommendedFeesType.Humanize()} ({context.CustomFeeRate} sats/vb)") From 907800c96852b6120430f2ecdf1eadb6f1f1dec0 Mon Sep 17 00:00:00 2001 From: Anon2148 Date: Mon, 16 Jun 2025 19:00:07 +0200 Subject: [PATCH 09/49] wip: bumpfee works partially, although the previous transaction is still active, had to be updated to a new state such as bumped or similar --- src/Pages/Withdrawals.razor | 24 +++++++++--------------- src/Shared/BumpfeeModal.razor | 22 +++++++++++----------- 2 files changed, 20 insertions(+), 26 deletions(-) diff --git a/src/Pages/Withdrawals.razor b/src/Pages/Withdrawals.razor index f37c1c43..d3196f59 100644 --- a/src/Pages/Withdrawals.razor +++ b/src/Pages/Withdrawals.razor @@ -883,6 +883,8 @@ private async Task CreateNewWithdrawal(WalletWithdrawalRequest newRequest) { + _selectedMempoolRecommendedFeesType = newRequest.MempoolRecommendedFeesType; + _customSatPerVbAmount = (long)newRequest.CustomFeeRate; if (newRequest.Wallet.IsHotWallet) { await GetData(); @@ -916,27 +918,19 @@ private async Task Bumpfee(WalletWithdrawalRequest request) { - try - { + try { await CreateNewWithdrawal(request); - StateHasChanged(); } catch { - ToastService.ShowError("Something went wrong"); + ToastService.ShowError("Could not bump the withdrawal"); } } - private async void SubmitBumpfeeModal() + private async void SubmitBumpfeeModal(WalletWithdrawalRequest newRequest) { - if (_bumpfeeRef != null && _bumpfeeRef.WithdrawalRequest != null) - { - _selectedMempoolRecommendedFeesType = _bumpfeeRef.WithdrawalRequest.MempoolRecommendedFeesType; - if (_bumpfeeRef.WithdrawalRequest.CustomFeeRate != null) - _customSatPerVbAmount = (long)_bumpfeeRef.WithdrawalRequest.CustomFeeRate; - if (_selectedRequest != null) { - await Bumpfee(_selectedRequest); - } else { - ToastService.ShowError("Error while getting the withdrawal"); - } + if (newRequest != null && _bumpfeeRef != null) { + newRequest.WalletId = _selectedRequest.WalletId; + newRequest.Wallet = await WalletRepository.GetById(newRequest.WalletId); + await Bumpfee(newRequest); await _bumpfeeRef.HideModal(); } else { ToastService.ShowError("Something went wrong"); diff --git a/src/Shared/BumpfeeModal.razor b/src/Shared/BumpfeeModal.razor index 3a6ba242..9e424336 100644 --- a/src/Shared/BumpfeeModal.razor +++ b/src/Shared/BumpfeeModal.razor @@ -3,13 +3,12 @@ @using System.Xml.Schema @using JSException = Microsoft.JSInterop.JSException @using System.Runtime.InteropServices.JavaScript -@inject IJSRuntime JS @if (WithdrawalRequest != null) { - Bumpfee tx: @(ChannelRequest?.Id ?? WithdrawalRequest?.Id ?? 0) + Bumpfee Withdrawal @@ -43,7 +42,6 @@ } -@inject IJSRuntime JsRuntime @inject IBitcoinService BitcoinService @inject IPriceConversionService PriceConversionService @inject INBXplorerService NBXplorerService @@ -58,15 +56,11 @@ public WalletWithdrawalRequest? WithdrawalRequest { get; set; } [Parameter] - public Action SubmitBumpfeeModal { get; set; } + public Action SubmitBumpfeeModal { get; set; } private decimal? _selectedWalletBalance; private decimal _btcPrice; - private static readonly decimal _minimumWithdrawalAmount = Constants.MINIMUM_WITHDRAWAL_BTC_AMOUNT; - - private decimal _amount { get; set; } = _minimumWithdrawalAmount; - private MempoolRecommendedFeesType _selectedMempoolRecommendedFeesType; private long _customSatPerVbAmount = 1; @@ -79,9 +73,15 @@ if (_customSatPerVbAmount <= _currentSatsPerVbAmount) { ToastService.ShowError("Fee must be greater than the current one"); } else { - WithdrawalRequest.MempoolRecommendedFeesType = _selectedMempoolRecommendedFeesType; - WithdrawalRequest.CustomFeeRate = _customSatPerVbAmount; - SubmitBumpfeeModal?.Invoke(); + WalletWithdrawalRequest newRequest = new WalletWithdrawalRequest(); + newRequest.Description = WithdrawalRequest.Description; + newRequest.DestinationAddress = WithdrawalRequest.DestinationAddress; + newRequest.Amount = WithdrawalRequest.Amount; + newRequest.Changeless = WithdrawalRequest.Changeless; + newRequest.WithdrawAllFunds = WithdrawalRequest.WithdrawAllFunds; + newRequest.MempoolRecommendedFeesType = _selectedMempoolRecommendedFeesType; + newRequest.CustomFeeRate = _customSatPerVbAmount; + SubmitBumpfeeModal?.Invoke(newRequest); } } From d3d8be1013041d69d74bea0c37ac89cff528758d Mon Sep 17 00:00:00 2001 From: scrpti Date: Tue, 17 Jun 2025 13:21:17 +0200 Subject: [PATCH 10/49] feat: add Parent Tx column and metadata handling in withdrawal requests --- .justfile | 12 ++++++------ docker/docker-compose.yml | 4 ++-- src/Pages/Withdrawals.razor | 26 +++++++++++++++++++++++++- src/Shared/BumpfeeModal.razor | 1 + 4 files changed, 34 insertions(+), 9 deletions(-) diff --git a/.justfile b/.justfile index 9608d2ab..3ea4134a 100644 --- a/.justfile +++ b/.justfile @@ -24,7 +24,7 @@ set dotenv-load := true # Aliases # ########### -alias i := install +# alias i := install alias b := build alias r := run alias t := test @@ -41,11 +41,11 @@ alias drm := docker-rm ####### # Everything necessary to install the project -[macos] -install: - #!/usr/bin/env bash - set -euxo pipefail - # Add your installation steps here +# [macos] +# install: +# #!/usr/bin/env bash +# set -euxo pipefail +# # Add your installation steps here ########## diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 13021b8a..9a9a4efe 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -36,8 +36,8 @@ services: NBXPLORER_CHAINS: "btc" NBXPLORER_BTCRPCUSER: "polaruser" NBXPLORER_BTCRPCPASSWORD: "polarpass" - NBXPLORER_BTCRPCURL: http://host.docker.internal:18443/ - NBXPLORER_BTCNODEENDPOINT: host.docker.internal:19444 + NBXPLORER_BTCRPCURL: http://172.17.0.1:18443/ + NBXPLORER_BTCNODEENDPOINT: 172.17.0.1:19444 command: ["--noauth"] volumes: - "bitcoin_datadir:/root/.bitcoin" diff --git a/src/Pages/Withdrawals.razor b/src/Pages/Withdrawals.razor index d3196f59..0c601a23 100644 --- a/src/Pages/Withdrawals.razor +++ b/src/Pages/Withdrawals.razor @@ -378,7 +378,7 @@ } - + @if (_isFinanceManager && context.Status == WalletWithdrawalRequestStatus.OnChainConfirmationPending) { @@ -392,6 +392,20 @@ } + + + @{ + if (!string.IsNullOrEmpty(context.RequestMetadata)) + { + var metadata = JsonConvert.DeserializeObject>(context.RequestMetadata); + if (metadata != null && metadata.TryGetValue("bumpOfTxId", out var parentTxId)) + { + Original tx + } + } + } + + @@ -525,6 +539,8 @@ public static readonly ColumnDefault CreationDate = new("Creation Date"); public static readonly ColumnDefault UpdateDate = new("Update Date"); public static readonly ColumnDefault Links = new("Links"); + public static readonly ColumnDefault Actions = new("Actions"); + public static readonly ColumnDefault ParentTx = new("Parent Tx"); } protected override async Task OnInitializedAsync() @@ -930,6 +946,14 @@ if (newRequest != null && _bumpfeeRef != null) { newRequest.WalletId = _selectedRequest.WalletId; newRequest.Wallet = await WalletRepository.GetById(newRequest.WalletId); + if (!string.IsNullOrEmpty(_selectedRequest?.TxId)) + { + var metadata = new Dictionary + { + { "bumpOfTxId", _selectedRequest.TxId } + }; + newRequest.RequestMetadata = JsonConvert.SerializeObject(metadata); + } await Bumpfee(newRequest); await _bumpfeeRef.HideModal(); } else { diff --git a/src/Shared/BumpfeeModal.razor b/src/Shared/BumpfeeModal.razor index 9e424336..470f9ab2 100644 --- a/src/Shared/BumpfeeModal.razor +++ b/src/Shared/BumpfeeModal.razor @@ -81,6 +81,7 @@ newRequest.WithdrawAllFunds = WithdrawalRequest.WithdrawAllFunds; newRequest.MempoolRecommendedFeesType = _selectedMempoolRecommendedFeesType; newRequest.CustomFeeRate = _customSatPerVbAmount; + newRequest.UserRequestorId = WithdrawalRequest.UserRequestorId; SubmitBumpfeeModal?.Invoke(newRequest); } } From 57c5d2cc13d9a0c99d04c83f1874b384326719d5 Mon Sep 17 00:00:00 2001 From: scrpti Date: Tue, 17 Jun 2025 13:23:05 +0200 Subject: [PATCH 11/49] fix: update Docker Compose to use host.docker.internal for BTC RPC URL and endpoint --- .justfile | 14 +++++++------- docker/docker-compose.yml | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.justfile b/.justfile index 3ea4134a..5502a5c6 100644 --- a/.justfile +++ b/.justfile @@ -24,7 +24,7 @@ set dotenv-load := true # Aliases # ########### -# alias i := install +alias i := install alias b := build alias r := run alias t := test @@ -40,12 +40,12 @@ alias drm := docker-rm # All# ####### -# Everything necessary to install the project -# [macos] -# install: -# #!/usr/bin/env bash -# set -euxo pipefail -# # Add your installation steps here +Everything necessary to install the project +[macos] +install: + #!/usr/bin/env bash + set -euxo pipefail + # Add your installation steps here ########## diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 9a9a4efe..13021b8a 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -36,8 +36,8 @@ services: NBXPLORER_CHAINS: "btc" NBXPLORER_BTCRPCUSER: "polaruser" NBXPLORER_BTCRPCPASSWORD: "polarpass" - NBXPLORER_BTCRPCURL: http://172.17.0.1:18443/ - NBXPLORER_BTCNODEENDPOINT: 172.17.0.1:19444 + NBXPLORER_BTCRPCURL: http://host.docker.internal:18443/ + NBXPLORER_BTCNODEENDPOINT: host.docker.internal:19444 command: ["--noauth"] volumes: - "bitcoin_datadir:/root/.bitcoin" From cca0645171a1270ffa944a1ffdbc15a4049cb51b Mon Sep 17 00:00:00 2001 From: Anon2148 Date: Thu, 19 Jun 2025 23:08:05 +0200 Subject: [PATCH 12/49] wip: adding utxos to bumped transaction, not working yet, problem with the database --- src/Data/Models/WalletWithdrawalRequest.cs | 11 +- src/Data/Repositories/FUTXORepository.cs | 14 + .../Interfaces/IFMUTXORepository.cs | 2 + ...250619174817_AddBumpingRequest.Designer.cs | 1309 +++++++++++++++++ .../20250619174817_AddBumpingRequest.cs | 48 + .../ApplicationDbContextModelSnapshot.cs | 11 + src/Pages/Withdrawals.razor | 11 + src/Shared/BumpfeeModal.razor | 5 +- 8 files changed, 1409 insertions(+), 2 deletions(-) create mode 100644 src/Migrations/20250619174817_AddBumpingRequest.Designer.cs create mode 100644 src/Migrations/20250619174817_AddBumpingRequest.cs diff --git a/src/Data/Models/WalletWithdrawalRequest.cs b/src/Data/Models/WalletWithdrawalRequest.cs index f7ac9624..188b009d 100644 --- a/src/Data/Models/WalletWithdrawalRequest.cs +++ b/src/Data/Models/WalletWithdrawalRequest.cs @@ -63,7 +63,12 @@ public enum WalletWithdrawalRequestStatus /// /// The PSBT is being signed by NodeGuard after all human required signatures have been collected /// - FinalizingPSBT = 7 + FinalizingPSBT = 7, + + /// + /// The tx was bumped by Replace by Fee (RBF) method + /// + Bumped = 8 } /// @@ -197,6 +202,10 @@ public bool Equals(WalletWithdrawalRequest? other) public Wallet Wallet { get; set; } + public int? BumpingId { get; set; } + + public WalletWithdrawalRequest? Bumping { get; set; } + public List WalletWithdrawalRequestPSBTs { get; set; } public List UTXOs { get; set; } diff --git a/src/Data/Repositories/FUTXORepository.cs b/src/Data/Repositories/FUTXORepository.cs index abea522e..273a9810 100644 --- a/src/Data/Repositories/FUTXORepository.cs +++ b/src/Data/Repositories/FUTXORepository.cs @@ -91,6 +91,20 @@ public async Task> GetAll() return _repository.Update(type, applicationDbContext); } + public (bool, string?) UpdateAll(List types) + { + (bool, string?) result = (true, null); + foreach (FMUTXO type in types) + { + result = Update(type); + if (!result.Item1) + { + return (false, "An error ocurred while updating the utxos"); + } + } + return result; + } + public async Task> GetLockedUTXOs(int? ignoredWalletWithdrawalRequestId = null, int? ignoredChannelOperationRequestId = null) { await using var applicationDbContext = await _dbContextFactory.CreateDbContextAsync(); diff --git a/src/Data/Repositories/Interfaces/IFMUTXORepository.cs b/src/Data/Repositories/Interfaces/IFMUTXORepository.cs index 28c79f7f..50aa3ad7 100644 --- a/src/Data/Repositories/Interfaces/IFMUTXORepository.cs +++ b/src/Data/Repositories/Interfaces/IFMUTXORepository.cs @@ -37,6 +37,8 @@ public interface IFMUTXORepository (bool, string?) Update(FMUTXO type); + (bool, string?) UpdateAll(List types); + /// /// Gets the current list of UTXOs locked on requests ChannelOperationRequest / WalletWithdrawalRequest by passing its id if wants to remove it from the resulting set /// diff --git a/src/Migrations/20250619174817_AddBumpingRequest.Designer.cs b/src/Migrations/20250619174817_AddBumpingRequest.Designer.cs new file mode 100644 index 00000000..a91c7878 --- /dev/null +++ b/src/Migrations/20250619174817_AddBumpingRequest.Designer.cs @@ -0,0 +1,1309 @@ +// +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("20250619174817_AddBumpingRequest")] + partial class AddBumpingRequest + { + /// + 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("BumpingId") + .HasColumnType("integer"); + + 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("BumpingId"); + + b.HasIndex("UserRequestorId"); + + b.HasIndex("WalletId"); + + b.ToTable("WalletWithdrawalRequests"); + }); + + 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.WalletWithdrawalRequest", "Bumping") + .WithMany() + .HasForeignKey("BumpingId"); + + 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("Bumping"); + + b.Navigation("UserRequestor"); + + b.Navigation("Wallet"); + }); + + 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("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/20250619174817_AddBumpingRequest.cs b/src/Migrations/20250619174817_AddBumpingRequest.cs new file mode 100644 index 00000000..bbcfaf93 --- /dev/null +++ b/src/Migrations/20250619174817_AddBumpingRequest.cs @@ -0,0 +1,48 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace NodeGuard.Migrations +{ + /// + public partial class AddBumpingRequest : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "BumpingId", + table: "WalletWithdrawalRequests", + type: "integer", + nullable: true); + + migrationBuilder.CreateIndex( + name: "IX_WalletWithdrawalRequests_BumpingId", + table: "WalletWithdrawalRequests", + column: "BumpingId"); + + migrationBuilder.AddForeignKey( + name: "FK_WalletWithdrawalRequests_WalletWithdrawalRequests_BumpingId", + table: "WalletWithdrawalRequests", + column: "BumpingId", + principalTable: "WalletWithdrawalRequests", + principalColumn: "Id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_WalletWithdrawalRequests_WalletWithdrawalRequests_BumpingId", + table: "WalletWithdrawalRequests"); + + migrationBuilder.DropIndex( + name: "IX_WalletWithdrawalRequests_BumpingId", + table: "WalletWithdrawalRequests"); + + migrationBuilder.DropColumn( + name: "BumpingId", + table: "WalletWithdrawalRequests"); + } + } +} diff --git a/src/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Migrations/ApplicationDbContextModelSnapshot.cs index 83c40790..7baab0b1 100644 --- a/src/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Migrations/ApplicationDbContextModelSnapshot.cs @@ -850,6 +850,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Amount") .HasColumnType("numeric"); + b.Property("BumpingId") + .HasColumnType("integer"); + b.Property("Changeless") .HasColumnType("boolean"); @@ -899,6 +902,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); + b.HasIndex("BumpingId"); + b.HasIndex("UserRequestorId"); b.HasIndex("WalletId"); @@ -1216,6 +1221,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("NodeGuard.Data.Models.WalletWithdrawalRequest", b => { + b.HasOne("NodeGuard.Data.Models.WalletWithdrawalRequest", "Bumping") + .WithMany() + .HasForeignKey("BumpingId"); + b.HasOne("NodeGuard.Data.Models.ApplicationUser", "UserRequestor") .WithMany("WalletWithdrawalRequests") .HasForeignKey("UserRequestorId"); @@ -1226,6 +1235,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + b.Navigation("Bumping"); + b.Navigation("UserRequestor"); b.Navigation("Wallet"); diff --git a/src/Pages/Withdrawals.razor b/src/Pages/Withdrawals.razor index 0c601a23..aa46a7fa 100644 --- a/src/Pages/Withdrawals.razor +++ b/src/Pages/Withdrawals.razor @@ -471,6 +471,7 @@ @inject ILocalStorageService LocalStorageService @inject IPriceConversionService PriceConversionService @inject INBXplorerService NBXplorerService +@inject IFMUTXORepository FMUTXORepository @code { [CascadingParameter] private ApplicationUser? LoggedUser { get; set; } @@ -1082,6 +1083,16 @@ ToastService.ShowSuccess("Withdrawal request created!"); + if (_selectedRequest.BumpingId != null) { + WalletWithdrawalRequest originalRequest = await WalletWithdrawalRequestRepository.GetById((int)_selectedRequest.BumpingId); + WalletWithdrawalRequest bumpedRequest = await WalletWithdrawalRequestRepository.GetById(_selectedRequest.Id); + + originalRequest.UTXOs.ForEach(u => u.WalletWithdrawalRequests.Add(bumpedRequest)); + bumpedRequest.UTXOs = originalRequest.UTXOs; + FMUTXORepository.UpdateAll(originalRequest.UTXOs); + WalletWithdrawalRequestRepository.Update(bumpedRequest); + } + if (_selectedUTXOs.Count > 0) { await CoinSelectionService.LockUTXOs(_selectedUTXOs, _selectedRequest, BitcoinRequestType.WalletWithdrawal); diff --git a/src/Shared/BumpfeeModal.razor b/src/Shared/BumpfeeModal.razor index 470f9ab2..552e645f 100644 --- a/src/Shared/BumpfeeModal.razor +++ b/src/Shared/BumpfeeModal.razor @@ -45,7 +45,9 @@ @inject IBitcoinService BitcoinService @inject IPriceConversionService PriceConversionService @inject INBXplorerService NBXplorerService +@inject IWalletWithdrawalRequestRepository WalletWithdrawalRequestRepository @inject IWalletRepository WalletRepository +@inject IFMUTXORepository FMUTXORepository @code { [Inject] public IToastService ToastService { get; set; } @@ -68,7 +70,6 @@ private async Task HandleOnClick() { - long _currentSatsPerVbAmount = (long?)await NBXplorerService.GetFeesByType(WithdrawalRequest.MempoolRecommendedFeesType) ?? 1; if (_customSatPerVbAmount <= _currentSatsPerVbAmount) { ToastService.ShowError("Fee must be greater than the current one"); @@ -82,6 +83,8 @@ newRequest.MempoolRecommendedFeesType = _selectedMempoolRecommendedFeesType; newRequest.CustomFeeRate = _customSatPerVbAmount; newRequest.UserRequestorId = WithdrawalRequest.UserRequestorId; + newRequest.BumpingId = WithdrawalRequest.Id; + newRequest.Bumping = WithdrawalRequest; SubmitBumpfeeModal?.Invoke(newRequest); } } From bb27615cbbbf7650d19120447546b6a771b196ce Mon Sep 17 00:00:00 2001 From: scrpti Date: Sun, 22 Jun 2025 16:33:47 +0200 Subject: [PATCH 13/49] feat: add handling for changeless transactions in bumpfee process to prevent fee increases --- src/Pages/Withdrawals.razor | 51 +++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/src/Pages/Withdrawals.razor b/src/Pages/Withdrawals.razor index 0c601a23..6b1bb5a3 100644 --- a/src/Pages/Withdrawals.razor +++ b/src/Pages/Withdrawals.razor @@ -814,6 +814,14 @@ "This transaction has already been mined."); return; } + + if (walletWithdrawalRequest.Changeless) + { + //Si la transaccion es changeless para bumpear la fee reduciremos el output + + ToastService.ShowWarning("This transaction is changeless and cannot be bumped. Please cancel it and create a new one with a higher fee."); + return; + } try { _selectedRequest = walletWithdrawalRequest; @@ -953,6 +961,49 @@ { "bumpOfTxId", _selectedRequest.TxId } }; newRequest.RequestMetadata = JsonConvert.SerializeObject(metadata); + @*} + if(newRequest.Changeless){ + //Si es changeless cambiaremos el output a output = output - diffFee donde diffFee es la diferencia entre el fee actual y el nuevo fee + var txInfo = await NBXplorerService.GetTransactionAsync(uint256.Parse(_selectedRequest.TxId)); + var tx = txInfo.Transaction; + long vSize = tx.GetVirtualSize(); + decimal oldFeeRate; + + if (_selectedRequest.MempoolRecommendedFeesType == MempoolRecommendedFeesType.CustomFee) + { + if (!_selectedRequest.CustomFeeRate.HasValue) + { + ToastService.ShowError("Custom fee rate not found."); + return; + } + + oldFeeRate = _selectedRequest.CustomFeeRate.Value; + } + else + { + var fetchedRate = await NBXplorerService.GetFeesByType(_selectedRequest.MempoolRecommendedFeesType); + if (!fetchedRate.HasValue) + { + ToastService.ShowError("Could not fetch original mempool fee rate."); + return; + } + + oldFeeRate = fetchedRate.Value; + } + + decimal oldFee = (vSize * oldFeeRate) / 100_000_000m; + decimal newFee = (vSize * _customSatPerVbAmount) / 100_000_000m; + decimal feeDiff = newFee - oldFee; + + if (newRequest.Amount <= feeDiff) + { + ToastService.ShowError("Cannot bump fee: the increased fee exceeds the amount being sent."); + return; + } + + newRequest.Amount -= feeDiff; + ToastService.ShowInfo($"Changeless bump: reduced output by {feeDiff:f8} BTC to increase the fee."); *@ + } await Bumpfee(newRequest); await _bumpfeeRef.HideModal(); From 081693b539f178aba36ac003cc78f43e4d238f00 Mon Sep 17 00:00:00 2001 From: Anon2148 Date: Mon, 23 Jun 2025 17:40:13 +0200 Subject: [PATCH 14/49] wip: modified how withdrawals register bumped withdrawals, already not working, I am trying to get the Utxos from the database but i cannot update them correctly --- src/Data/Models/WalletWithdrawalRequest.cs | 2 - src/Data/Repositories/FUTXORepository.cs | 28 +++++++++++ .../Interfaces/IFMUTXORepository.cs | 5 ++ .../20250619174817_AddBumpingRequest.cs | 48 ------------------- ...50623153621_AddBumpingRequest.Designer.cs} | 10 +--- .../20250623153621_AddBumpingRequest.cs | 28 +++++++++++ .../ApplicationDbContextModelSnapshot.cs | 8 ---- src/Pages/Withdrawals.razor | 11 +++-- src/Shared/BumpfeeModal.razor | 1 - 9 files changed, 70 insertions(+), 71 deletions(-) delete mode 100644 src/Migrations/20250619174817_AddBumpingRequest.cs rename src/Migrations/{20250619174817_AddBumpingRequest.Designer.cs => 20250623153621_AddBumpingRequest.Designer.cs} (99%) create mode 100644 src/Migrations/20250623153621_AddBumpingRequest.cs diff --git a/src/Data/Models/WalletWithdrawalRequest.cs b/src/Data/Models/WalletWithdrawalRequest.cs index 188b009d..01ac9922 100644 --- a/src/Data/Models/WalletWithdrawalRequest.cs +++ b/src/Data/Models/WalletWithdrawalRequest.cs @@ -204,8 +204,6 @@ public bool Equals(WalletWithdrawalRequest? other) public int? BumpingId { get; set; } - public WalletWithdrawalRequest? Bumping { get; set; } - public List WalletWithdrawalRequestPSBTs { get; set; } public List UTXOs { get; set; } diff --git a/src/Data/Repositories/FUTXORepository.cs b/src/Data/Repositories/FUTXORepository.cs index 273a9810..d11e04cc 100644 --- a/src/Data/Repositories/FUTXORepository.cs +++ b/src/Data/Repositories/FUTXORepository.cs @@ -105,6 +105,34 @@ public async Task> GetAll() return result; } + public async Task> GetLockedUTXOsByRequestId(int? walletWithdrawalRequestId = null, int? channelOperationRequestId = null) + { + await using var applicationDbContext = await _dbContextFactory.CreateDbContextAsync(); + + var walletWithdrawalRequestsLockedUTXOs = new List(); + if (walletWithdrawalRequestId != null) + { + walletWithdrawalRequestsLockedUTXOs = await applicationDbContext.WalletWithdrawalRequests + .Include(x => x.UTXOs) + .Where(x => x.Id == walletWithdrawalRequestId) + .SelectMany(x => x.UTXOs).ToListAsync(); + } + + var channelOperationRequestsLockedUTXOs = new List(); + + if (channelOperationRequestId != null) + { + walletWithdrawalRequestsLockedUTXOs = await applicationDbContext.ChannelOperationRequests + .Include(x => x.Utxos) + .Where(x => x.Id == walletWithdrawalRequestId) + .SelectMany(x => x.Utxos).ToListAsync(); + } + + var result = walletWithdrawalRequestsLockedUTXOs.Union(channelOperationRequestsLockedUTXOs).ToList(); + + return result; + } + public async Task> GetLockedUTXOs(int? ignoredWalletWithdrawalRequestId = null, int? ignoredChannelOperationRequestId = null) { await using var applicationDbContext = await _dbContextFactory.CreateDbContextAsync(); diff --git a/src/Data/Repositories/Interfaces/IFMUTXORepository.cs b/src/Data/Repositories/Interfaces/IFMUTXORepository.cs index 50aa3ad7..c7e97cff 100644 --- a/src/Data/Repositories/Interfaces/IFMUTXORepository.cs +++ b/src/Data/Repositories/Interfaces/IFMUTXORepository.cs @@ -44,4 +44,9 @@ public interface IFMUTXORepository /// /// Task> GetLockedUTXOs(int? ignoredWalletWithdrawalRequestId = null, int? ignoredChannelOperationRequestId = null); + /// + /// Gets the current list of UTXOs locked on requests ChannelOperationRequest / WalletWithdrawalRequest by passing its id + /// + /// + Task> GetLockedUTXOsByRequestId(int? walletWithdrawalRequestId = null, int? channelOperationRequestId = null); } \ No newline at end of file diff --git a/src/Migrations/20250619174817_AddBumpingRequest.cs b/src/Migrations/20250619174817_AddBumpingRequest.cs deleted file mode 100644 index bbcfaf93..00000000 --- a/src/Migrations/20250619174817_AddBumpingRequest.cs +++ /dev/null @@ -1,48 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace NodeGuard.Migrations -{ - /// - public partial class AddBumpingRequest : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "BumpingId", - table: "WalletWithdrawalRequests", - type: "integer", - nullable: true); - - migrationBuilder.CreateIndex( - name: "IX_WalletWithdrawalRequests_BumpingId", - table: "WalletWithdrawalRequests", - column: "BumpingId"); - - migrationBuilder.AddForeignKey( - name: "FK_WalletWithdrawalRequests_WalletWithdrawalRequests_BumpingId", - table: "WalletWithdrawalRequests", - column: "BumpingId", - principalTable: "WalletWithdrawalRequests", - principalColumn: "Id"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropForeignKey( - name: "FK_WalletWithdrawalRequests_WalletWithdrawalRequests_BumpingId", - table: "WalletWithdrawalRequests"); - - migrationBuilder.DropIndex( - name: "IX_WalletWithdrawalRequests_BumpingId", - table: "WalletWithdrawalRequests"); - - migrationBuilder.DropColumn( - name: "BumpingId", - table: "WalletWithdrawalRequests"); - } - } -} diff --git a/src/Migrations/20250619174817_AddBumpingRequest.Designer.cs b/src/Migrations/20250623153621_AddBumpingRequest.Designer.cs similarity index 99% rename from src/Migrations/20250619174817_AddBumpingRequest.Designer.cs rename to src/Migrations/20250623153621_AddBumpingRequest.Designer.cs index a91c7878..6d234476 100644 --- a/src/Migrations/20250619174817_AddBumpingRequest.Designer.cs +++ b/src/Migrations/20250623153621_AddBumpingRequest.Designer.cs @@ -14,7 +14,7 @@ namespace NodeGuard.Migrations { [DbContext(typeof(ApplicationDbContext))] - [Migration("20250619174817_AddBumpingRequest")] + [Migration("20250623153621_AddBumpingRequest")] partial class AddBumpingRequest { /// @@ -905,8 +905,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.HasKey("Id"); - b.HasIndex("BumpingId"); - b.HasIndex("UserRequestorId"); b.HasIndex("WalletId"); @@ -1224,10 +1222,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) modelBuilder.Entity("NodeGuard.Data.Models.WalletWithdrawalRequest", b => { - b.HasOne("NodeGuard.Data.Models.WalletWithdrawalRequest", "Bumping") - .WithMany() - .HasForeignKey("BumpingId"); - b.HasOne("NodeGuard.Data.Models.ApplicationUser", "UserRequestor") .WithMany("WalletWithdrawalRequests") .HasForeignKey("UserRequestorId"); @@ -1238,8 +1232,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.Navigation("Bumping"); - b.Navigation("UserRequestor"); b.Navigation("Wallet"); diff --git a/src/Migrations/20250623153621_AddBumpingRequest.cs b/src/Migrations/20250623153621_AddBumpingRequest.cs new file mode 100644 index 00000000..45b97f5d --- /dev/null +++ b/src/Migrations/20250623153621_AddBumpingRequest.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace NodeGuard.Migrations +{ + /// + public partial class AddBumpingRequest : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "BumpingId", + table: "WalletWithdrawalRequests", + type: "integer", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "BumpingId", + table: "WalletWithdrawalRequests"); + } + } +} diff --git a/src/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Migrations/ApplicationDbContextModelSnapshot.cs index 7baab0b1..7dbb305f 100644 --- a/src/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Migrations/ApplicationDbContextModelSnapshot.cs @@ -902,8 +902,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); - b.HasIndex("BumpingId"); - b.HasIndex("UserRequestorId"); b.HasIndex("WalletId"); @@ -1221,10 +1219,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("NodeGuard.Data.Models.WalletWithdrawalRequest", b => { - b.HasOne("NodeGuard.Data.Models.WalletWithdrawalRequest", "Bumping") - .WithMany() - .HasForeignKey("BumpingId"); - b.HasOne("NodeGuard.Data.Models.ApplicationUser", "UserRequestor") .WithMany("WalletWithdrawalRequests") .HasForeignKey("UserRequestorId"); @@ -1235,8 +1229,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.Navigation("Bumping"); - b.Navigation("UserRequestor"); b.Navigation("Wallet"); diff --git a/src/Pages/Withdrawals.razor b/src/Pages/Withdrawals.razor index aa46a7fa..530914c6 100644 --- a/src/Pages/Withdrawals.razor +++ b/src/Pages/Withdrawals.razor @@ -1085,11 +1085,16 @@ if (_selectedRequest.BumpingId != null) { WalletWithdrawalRequest originalRequest = await WalletWithdrawalRequestRepository.GetById((int)_selectedRequest.BumpingId); + List mUTXOs = await FMUTXORepository.GetLockedUTXOsByRequestId(originalRequest.Id); WalletWithdrawalRequest bumpedRequest = await WalletWithdrawalRequestRepository.GetById(_selectedRequest.Id); - originalRequest.UTXOs.ForEach(u => u.WalletWithdrawalRequests.Add(bumpedRequest)); - bumpedRequest.UTXOs = originalRequest.UTXOs; - FMUTXORepository.UpdateAll(originalRequest.UTXOs); + mUTXOs.ForEach(u => { + if (u.WalletWithdrawalRequests.Contains(originalRequest)) { + u.WalletWithdrawalRequests.Add(bumpedRequest); + bumpedRequest.UTXOs.Add(u); + } + }); + FMUTXORepository.UpdateAll(mUTXOs); WalletWithdrawalRequestRepository.Update(bumpedRequest); } diff --git a/src/Shared/BumpfeeModal.razor b/src/Shared/BumpfeeModal.razor index 552e645f..7698cb30 100644 --- a/src/Shared/BumpfeeModal.razor +++ b/src/Shared/BumpfeeModal.razor @@ -84,7 +84,6 @@ newRequest.CustomFeeRate = _customSatPerVbAmount; newRequest.UserRequestorId = WithdrawalRequest.UserRequestorId; newRequest.BumpingId = WithdrawalRequest.Id; - newRequest.Bumping = WithdrawalRequest; SubmitBumpfeeModal?.Invoke(newRequest); } } From 3e0103d75ee6c8ad7ab53a8352997606bf35ce94 Mon Sep 17 00:00:00 2001 From: Anon2148 Date: Tue, 24 Jun 2025 17:35:16 +0200 Subject: [PATCH 15/49] wip: keep working on updating the UTXOs, the request could be updated but the UTXOs does not update with the new WithrawalRequest because an exception of pk_keywallet unique constraint arises --- src/Data/Repositories/FUTXORepository.cs | 14 -------------- .../Repositories/Interfaces/IFMUTXORepository.cs | 2 -- src/Pages/Withdrawals.razor | 14 ++++++++++---- 3 files changed, 10 insertions(+), 20 deletions(-) diff --git a/src/Data/Repositories/FUTXORepository.cs b/src/Data/Repositories/FUTXORepository.cs index d11e04cc..2b6c1c15 100644 --- a/src/Data/Repositories/FUTXORepository.cs +++ b/src/Data/Repositories/FUTXORepository.cs @@ -91,20 +91,6 @@ public async Task> GetAll() return _repository.Update(type, applicationDbContext); } - public (bool, string?) UpdateAll(List types) - { - (bool, string?) result = (true, null); - foreach (FMUTXO type in types) - { - result = Update(type); - if (!result.Item1) - { - return (false, "An error ocurred while updating the utxos"); - } - } - return result; - } - public async Task> GetLockedUTXOsByRequestId(int? walletWithdrawalRequestId = null, int? channelOperationRequestId = null) { await using var applicationDbContext = await _dbContextFactory.CreateDbContextAsync(); diff --git a/src/Data/Repositories/Interfaces/IFMUTXORepository.cs b/src/Data/Repositories/Interfaces/IFMUTXORepository.cs index c7e97cff..ef78613a 100644 --- a/src/Data/Repositories/Interfaces/IFMUTXORepository.cs +++ b/src/Data/Repositories/Interfaces/IFMUTXORepository.cs @@ -37,8 +37,6 @@ public interface IFMUTXORepository (bool, string?) Update(FMUTXO type); - (bool, string?) UpdateAll(List types); - /// /// Gets the current list of UTXOs locked on requests ChannelOperationRequest / WalletWithdrawalRequest by passing its id if wants to remove it from the resulting set /// diff --git a/src/Pages/Withdrawals.razor b/src/Pages/Withdrawals.razor index 530914c6..db0f8789 100644 --- a/src/Pages/Withdrawals.razor +++ b/src/Pages/Withdrawals.razor @@ -1088,13 +1088,19 @@ List mUTXOs = await FMUTXORepository.GetLockedUTXOsByRequestId(originalRequest.Id); WalletWithdrawalRequest bumpedRequest = await WalletWithdrawalRequestRepository.GetById(_selectedRequest.Id); + if (bumpedRequest.UTXOs == null) { + bumpedRequest.UTXOs = new List(); + } + mUTXOs.ForEach(u => { - if (u.WalletWithdrawalRequests.Contains(originalRequest)) { - u.WalletWithdrawalRequests.Add(bumpedRequest); - bumpedRequest.UTXOs.Add(u); + if (u.WalletWithdrawalRequests == null) { + u.WalletWithdrawalRequests = new List(); } + + u.WalletWithdrawalRequests.Add(bumpedRequest); + bumpedRequest.UTXOs.Add(u); + FMUTXORepository.Update(u); }); - FMUTXORepository.UpdateAll(mUTXOs); WalletWithdrawalRequestRepository.Update(bumpedRequest); } From f6cfe5025b004424de935be0bae87729b780f000 Mon Sep 17 00:00:00 2001 From: Anon2148 Date: Thu, 26 Jun 2025 18:23:51 +0200 Subject: [PATCH 16/49] feat: added bumping object to WalletWithdrawalRequest and added utxos functionality, also set optInRBF in tx builder when generating template PSBT --- .justfile | 2 +- src/Data/Models/WalletWithdrawalRequest.cs | 2 + .../WalletWithdrawalRequestRepository.cs | 1 + .../20250623153621_AddBumpingRequest.cs | 28 ----------- ...50626132159_AddBumpingRequest.Designer.cs} | 10 +++- .../20250626132159_AddBumpingRequest.cs | 48 +++++++++++++++++++ .../ApplicationDbContextModelSnapshot.cs | 8 ++++ src/Pages/Withdrawals.razor | 37 +++++++------- src/Services/BitcoinService.cs | 3 +- 9 files changed, 88 insertions(+), 51 deletions(-) delete mode 100644 src/Migrations/20250623153621_AddBumpingRequest.cs rename src/Migrations/{20250623153621_AddBumpingRequest.Designer.cs => 20250626132159_AddBumpingRequest.Designer.cs} (99%) create mode 100644 src/Migrations/20250626132159_AddBumpingRequest.cs diff --git a/.justfile b/.justfile index 5502a5c6..9608d2ab 100644 --- a/.justfile +++ b/.justfile @@ -40,7 +40,7 @@ alias drm := docker-rm # All# ####### -Everything necessary to install the project +# Everything necessary to install the project [macos] install: #!/usr/bin/env bash diff --git a/src/Data/Models/WalletWithdrawalRequest.cs b/src/Data/Models/WalletWithdrawalRequest.cs index 01ac9922..188b009d 100644 --- a/src/Data/Models/WalletWithdrawalRequest.cs +++ b/src/Data/Models/WalletWithdrawalRequest.cs @@ -204,6 +204,8 @@ public bool Equals(WalletWithdrawalRequest? other) public int? BumpingId { get; set; } + public WalletWithdrawalRequest? Bumping { get; set; } + public List WalletWithdrawalRequestPSBTs { get; set; } public List UTXOs { get; set; } diff --git a/src/Data/Repositories/WalletWithdrawalRequestRepository.cs b/src/Data/Repositories/WalletWithdrawalRequestRepository.cs index 3ba90d5e..64a13683 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.UTXOs) .SingleOrDefaultAsync(x => x.Id == id); return request; diff --git a/src/Migrations/20250623153621_AddBumpingRequest.cs b/src/Migrations/20250623153621_AddBumpingRequest.cs deleted file mode 100644 index 45b97f5d..00000000 --- a/src/Migrations/20250623153621_AddBumpingRequest.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace NodeGuard.Migrations -{ - /// - public partial class AddBumpingRequest : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "BumpingId", - table: "WalletWithdrawalRequests", - type: "integer", - nullable: true); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "BumpingId", - table: "WalletWithdrawalRequests"); - } - } -} diff --git a/src/Migrations/20250623153621_AddBumpingRequest.Designer.cs b/src/Migrations/20250626132159_AddBumpingRequest.Designer.cs similarity index 99% rename from src/Migrations/20250623153621_AddBumpingRequest.Designer.cs rename to src/Migrations/20250626132159_AddBumpingRequest.Designer.cs index 6d234476..ab52d745 100644 --- a/src/Migrations/20250623153621_AddBumpingRequest.Designer.cs +++ b/src/Migrations/20250626132159_AddBumpingRequest.Designer.cs @@ -14,7 +14,7 @@ namespace NodeGuard.Migrations { [DbContext(typeof(ApplicationDbContext))] - [Migration("20250623153621_AddBumpingRequest")] + [Migration("20250626132159_AddBumpingRequest")] partial class AddBumpingRequest { /// @@ -905,6 +905,8 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.HasKey("Id"); + b.HasIndex("BumpingId"); + b.HasIndex("UserRequestorId"); b.HasIndex("WalletId"); @@ -1222,6 +1224,10 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) modelBuilder.Entity("NodeGuard.Data.Models.WalletWithdrawalRequest", b => { + b.HasOne("NodeGuard.Data.Models.WalletWithdrawalRequest", "Bumping") + .WithMany() + .HasForeignKey("BumpingId"); + b.HasOne("NodeGuard.Data.Models.ApplicationUser", "UserRequestor") .WithMany("WalletWithdrawalRequests") .HasForeignKey("UserRequestorId"); @@ -1232,6 +1238,8 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + b.Navigation("Bumping"); + b.Navigation("UserRequestor"); b.Navigation("Wallet"); diff --git a/src/Migrations/20250626132159_AddBumpingRequest.cs b/src/Migrations/20250626132159_AddBumpingRequest.cs new file mode 100644 index 00000000..bbcfaf93 --- /dev/null +++ b/src/Migrations/20250626132159_AddBumpingRequest.cs @@ -0,0 +1,48 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace NodeGuard.Migrations +{ + /// + public partial class AddBumpingRequest : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "BumpingId", + table: "WalletWithdrawalRequests", + type: "integer", + nullable: true); + + migrationBuilder.CreateIndex( + name: "IX_WalletWithdrawalRequests_BumpingId", + table: "WalletWithdrawalRequests", + column: "BumpingId"); + + migrationBuilder.AddForeignKey( + name: "FK_WalletWithdrawalRequests_WalletWithdrawalRequests_BumpingId", + table: "WalletWithdrawalRequests", + column: "BumpingId", + principalTable: "WalletWithdrawalRequests", + principalColumn: "Id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_WalletWithdrawalRequests_WalletWithdrawalRequests_BumpingId", + table: "WalletWithdrawalRequests"); + + migrationBuilder.DropIndex( + name: "IX_WalletWithdrawalRequests_BumpingId", + table: "WalletWithdrawalRequests"); + + migrationBuilder.DropColumn( + name: "BumpingId", + table: "WalletWithdrawalRequests"); + } + } +} diff --git a/src/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Migrations/ApplicationDbContextModelSnapshot.cs index 7dbb305f..7baab0b1 100644 --- a/src/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Migrations/ApplicationDbContextModelSnapshot.cs @@ -902,6 +902,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); + b.HasIndex("BumpingId"); + b.HasIndex("UserRequestorId"); b.HasIndex("WalletId"); @@ -1219,6 +1221,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("NodeGuard.Data.Models.WalletWithdrawalRequest", b => { + b.HasOne("NodeGuard.Data.Models.WalletWithdrawalRequest", "Bumping") + .WithMany() + .HasForeignKey("BumpingId"); + b.HasOne("NodeGuard.Data.Models.ApplicationUser", "UserRequestor") .WithMany("WalletWithdrawalRequests") .HasForeignKey("UserRequestorId"); @@ -1229,6 +1235,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + b.Navigation("Bumping"); + b.Navigation("UserRequestor"); b.Navigation("Wallet"); diff --git a/src/Pages/Withdrawals.razor b/src/Pages/Withdrawals.razor index 1695dcfb..4f25512f 100644 --- a/src/Pages/Withdrawals.razor +++ b/src/Pages/Withdrawals.razor @@ -816,14 +816,6 @@ return; } - if (walletWithdrawalRequest.Changeless) - { - //Si la transaccion es changeless para bumpear la fee reduciremos el output - - ToastService.ShowWarning("This transaction is changeless and cannot be bumped. Please cancel it and create a new one with a higher fee."); - return; - } - try { _selectedRequest = walletWithdrawalRequest; @@ -955,6 +947,7 @@ if (newRequest != null && _bumpfeeRef != null) { newRequest.WalletId = _selectedRequest.WalletId; newRequest.Wallet = await WalletRepository.GetById(newRequest.WalletId); + decimal newFeeRate = (decimal)(newRequest.CustomFeeRate ?? 0m); if (!string.IsNullOrEmpty(_selectedRequest?.TxId)) { var metadata = new Dictionary @@ -962,9 +955,10 @@ { "bumpOfTxId", _selectedRequest.TxId } }; newRequest.RequestMetadata = JsonConvert.SerializeObject(metadata); - @*} - if(newRequest.Changeless){ - //Si es changeless cambiaremos el output a output = output - diffFee donde diffFee es la diferencia entre el fee actual y el nuevo fee + } + if(newRequest.Changeless) + { + //Whether we change the output to output = output - diffFee where diffFee is the difference between the current fee and the new fee var txInfo = await NBXplorerService.GetTransactionAsync(uint256.Parse(_selectedRequest.TxId)); var tx = txInfo.Transaction; long vSize = tx.GetVirtualSize(); @@ -993,7 +987,7 @@ } decimal oldFee = (vSize * oldFeeRate) / 100_000_000m; - decimal newFee = (vSize * _customSatPerVbAmount) / 100_000_000m; + decimal newFee = (vSize * newFeeRate) / 100_000_000m; decimal feeDiff = newFee - oldFee; if (newRequest.Amount <= feeDiff) @@ -1003,7 +997,7 @@ } newRequest.Amount -= feeDiff; - ToastService.ShowInfo($"Changeless bump: reduced output by {feeDiff:f8} BTC to increase the fee."); *@ + ToastService.ShowInfo($"Changeless bump: reduced output by {feeDiff:f8} BTC to increase the fee."); } await Bumpfee(newRequest); @@ -1013,6 +1007,7 @@ } } + private async Task ApproveRequestDelegate() { if (_selectedRequest != null) @@ -1143,15 +1138,17 @@ bumpedRequest.UTXOs = new List(); } + bumpedRequest.UTXOs = mUTXOs; + List outpoints = new List(); mUTXOs.ForEach(u => { - if (u.WalletWithdrawalRequests == null) { - u.WalletWithdrawalRequests = new List(); - } - - u.WalletWithdrawalRequests.Add(bumpedRequest); - bumpedRequest.UTXOs.Add(u); - FMUTXORepository.Update(u); + OutPoint o; + if (OutPoint.TryParse($"{u.TxId}:{u.OutputIndex}", out o)) + outpoints.Add(o); + else + throw new Exception(); // To-Do }); + _selectedUTXOs = await CoinSelectionService.GetUTXOsByOutpointAsync(originalRequest.Wallet.GetDerivationStrategy(), outpoints); + WalletWithdrawalRequestRepository.Update(bumpedRequest); } diff --git a/src/Services/BitcoinService.cs b/src/Services/BitcoinService.cs index 8dbee02f..67a3a155 100644 --- a/src/Services/BitcoinService.cs +++ b/src/Services/BitcoinService.cs @@ -223,7 +223,8 @@ await _coinSelectionService.GetTxInputCoins(availableUTXOs, walletWithdrawalRequ builder.SetSigningOptions(SigHash.All) .SetChange(changeAddress.Address) - .SendEstimatedFees(feeRateResult); + .SendEstimatedFees(feeRateResult) + .SetOptInRBF(true); // We preserve the output order when testing so the psbt doesn't change var command = Assembly.GetEntryAssembly()?.GetName().Name?.ToLowerInvariant(); From d7146d396d89ab2537d168ad5a6f36b0dea644d1 Mon Sep 17 00:00:00 2001 From: Rubenro01 Date: Sat, 28 Jun 2025 11:56:57 +0200 Subject: [PATCH 17/49] feat: add display of last fee used and its type in bumpfee modal --- src/Shared/BumpfeeModal.razor | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/Shared/BumpfeeModal.razor b/src/Shared/BumpfeeModal.razor index 7698cb30..a1127968 100644 --- a/src/Shared/BumpfeeModal.razor +++ b/src/Shared/BumpfeeModal.razor @@ -29,6 +29,35 @@
@(_selectedMempoolRecommendedFeesType != MempoolRecommendedFeesType.CustomFee ? "Fees may change by the time the request is first signed" : "") + Last fee used +
+ + + +
+ @(_selectedMempoolRecommendedFeesType != MempoolRecommendedFeesType.CustomFee ? "Fees must be greater than the current one" : "") + From 3460ca679b18ce57740d881a7b459fc3f29d9ca3 Mon Sep 17 00:00:00 2001 From: Rubenro01 Date: Tue, 1 Jul 2025 09:50:18 +0200 Subject: [PATCH 18/49] fix: improve error message for insufficient fee in bumpfee modal --- src/Shared/BumpfeeModal.razor | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Shared/BumpfeeModal.razor b/src/Shared/BumpfeeModal.razor index a1127968..893e4d8c 100644 --- a/src/Shared/BumpfeeModal.razor +++ b/src/Shared/BumpfeeModal.razor @@ -101,7 +101,7 @@ { long _currentSatsPerVbAmount = (long?)await NBXplorerService.GetFeesByType(WithdrawalRequest.MempoolRecommendedFeesType) ?? 1; if (_customSatPerVbAmount <= _currentSatsPerVbAmount) { - ToastService.ShowError("Fee must be greater than the current one"); + ToastService.ShowError($"Fee must be greater than the current one ({_currentSatsPerVbAmount} sats/vB)"); } else { WalletWithdrawalRequest newRequest = new WalletWithdrawalRequest(); newRequest.Description = WithdrawalRequest.Description; From a557b0ff42bde40a328437678893cb54b3475d8a Mon Sep 17 00:00:00 2001 From: scrpti Date: Tue, 1 Jul 2025 14:23:45 +0200 Subject: [PATCH 19/49] feat: enhance bump fee functionality with error handling for changeless transactions and improved fee rate selection --- src/Pages/Withdrawals.razor | 115 ++++++++++++++++++------------------ 1 file changed, 59 insertions(+), 56 deletions(-) diff --git a/src/Pages/Withdrawals.razor b/src/Pages/Withdrawals.razor index 4f25512f..48ec6a95 100644 --- a/src/Pages/Withdrawals.razor +++ b/src/Pages/Withdrawals.razor @@ -378,20 +378,6 @@ }
- - - @if (_isFinanceManager && context.Status == WalletWithdrawalRequestStatus.OnChainConfirmationPending) - { - - - - - Bump fee - - - } - - @{ @@ -406,6 +392,21 @@ } + + + @if (_isFinanceManager && context.Status == WalletWithdrawalRequestStatus.OnChainConfirmationPending) + { + + + + + Bump fee + + + } + + + @@ -816,6 +817,13 @@ return; } + //If its changeless show a toast + if (walletWithdrawalRequest.Changeless) + { + ToastService.ShowError("Fee bumping is not supported for changeless transactions. Please create a new withdrawal with higher fees instead."); + return; + } + try { _selectedRequest = walletWithdrawalRequest; @@ -947,7 +955,19 @@ if (newRequest != null && _bumpfeeRef != null) { newRequest.WalletId = _selectedRequest.WalletId; newRequest.Wallet = await WalletRepository.GetById(newRequest.WalletId); - decimal newFeeRate = (decimal)(newRequest.CustomFeeRate ?? 0m); + + // Use either the custom fee rate or the recommended fee rate based on mempool fee type + decimal newFeeRate; + if (newRequest.MempoolRecommendedFeesType == MempoolRecommendedFeesType.CustomFee && newRequest.CustomFeeRate.HasValue) + { + newFeeRate = (decimal)newRequest.CustomFeeRate.Value; + } + else + { + // Use the recommended fee rate from NBXplorer service based on the selected mempool fee type + var recommendedFeeRate = await GetRecommendedFeeRate(newRequest.MempoolRecommendedFeesType); + newFeeRate = (decimal)recommendedFeeRate; + } if (!string.IsNullOrEmpty(_selectedRequest?.TxId)) { var metadata = new Dictionary @@ -958,47 +978,25 @@ } if(newRequest.Changeless) { - //Whether we change the output to output = output - diffFee where diffFee is the difference between the current fee and the new fee - var txInfo = await NBXplorerService.GetTransactionAsync(uint256.Parse(_selectedRequest.TxId)); - var tx = txInfo.Transaction; - long vSize = tx.GetVirtualSize(); - decimal oldFeeRate; - - if (_selectedRequest.MempoolRecommendedFeesType == MempoolRecommendedFeesType.CustomFee) - { - if (!_selectedRequest.CustomFeeRate.HasValue) - { - ToastService.ShowError("Custom fee rate not found."); - return; - } - - oldFeeRate = _selectedRequest.CustomFeeRate.Value; - } - else - { - var fetchedRate = await NBXplorerService.GetFeesByType(_selectedRequest.MempoolRecommendedFeesType); - if (!fetchedRate.HasValue) - { - ToastService.ShowError("Could not fetch original mempool fee rate."); - return; - } - - oldFeeRate = fetchedRate.Value; - } - - decimal oldFee = (vSize * oldFeeRate) / 100_000_000m; - decimal newFee = (vSize * newFeeRate) / 100_000_000m; - decimal feeDiff = newFee - oldFee; - - if (newRequest.Amount <= feeDiff) - { - ToastService.ShowError("Cannot bump fee: the increased fee exceeds the amount being sent."); - return; - } - - newRequest.Amount -= feeDiff; - ToastService.ShowInfo($"Changeless bump: reduced output by {feeDiff:f8} BTC to increase the fee."); - + /* + * CHANGELESS FEE BUMPING IS NOT SUPPORTED + * + * Previous attempts to implement changeless fee bumping have failed due to: + * 1. Double fee calculation issues - the UI calculates fees and NBitcoin also calculates fees, + * leading to "NotEnoughFundsException" when the total exceeds available input value. + * 2. Precision mismatches between decimal calculations in the UI and NBitcoin's satoshi-based calculations. + * 3. Complex UTXO selection and validation logic that doesn't account for changeless scenarios properly. + * 4. NBitcoin's transaction builder expecting standard fee estimation patterns, not explicit fee amounts. + * + * Attempted solutions that didn't work: + * - Calculating fees in satoshis for precision matching + * - Explicitly setting fees with builder.SendFees() + * - Validating UTXO availability with unconfirmed UTXOs + * - Custom fee calculation logic for changeless transactions + * + * For now, changeless transactions cannot be fee-bumped. Users should create a new + * transaction with higher fees instead. + */ } await Bumpfee(newRequest); await _bumpfeeRef.HideModal(); @@ -1007,6 +1005,11 @@ } } + private async Task GetRecommendedFeeRate(MempoolRecommendedFeesType feeType) + { + return (long?)await NBXplorerService.GetFeesByType(feeType) ?? 1; + } + private async Task ApproveRequestDelegate() { From 17c0a4ee01a792ee18c4a0b2df360c89ebb7568f Mon Sep 17 00:00:00 2001 From: Anon2148 Date: Fri, 4 Jul 2025 16:18:31 +0200 Subject: [PATCH 20/49] feat: added bumped state and modified logic when bumping transaction to update state correctly --- ....Designer.cs => 20250702080756_BumpingRequest.Designer.cs} | 4 ++-- ..._AddBumpingRequest.cs => 20250702080756_BumpingRequest.cs} | 2 +- src/Pages/Withdrawals.razor | 2 ++ src/Proto/nodeguard.proto | 1 + src/Rpc/NodeGuardService.cs | 1 + 5 files changed, 7 insertions(+), 3 deletions(-) rename src/Migrations/{20250626132159_AddBumpingRequest.Designer.cs => 20250702080756_BumpingRequest.Designer.cs} (99%) rename src/Migrations/{20250626132159_AddBumpingRequest.cs => 20250702080756_BumpingRequest.cs} (96%) diff --git a/src/Migrations/20250626132159_AddBumpingRequest.Designer.cs b/src/Migrations/20250702080756_BumpingRequest.Designer.cs similarity index 99% rename from src/Migrations/20250626132159_AddBumpingRequest.Designer.cs rename to src/Migrations/20250702080756_BumpingRequest.Designer.cs index ab52d745..ffdd77b5 100644 --- a/src/Migrations/20250626132159_AddBumpingRequest.Designer.cs +++ b/src/Migrations/20250702080756_BumpingRequest.Designer.cs @@ -14,8 +14,8 @@ namespace NodeGuard.Migrations { [DbContext(typeof(ApplicationDbContext))] - [Migration("20250626132159_AddBumpingRequest")] - partial class AddBumpingRequest + [Migration("20250702080756_BumpingRequest")] + partial class BumpingRequest { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) diff --git a/src/Migrations/20250626132159_AddBumpingRequest.cs b/src/Migrations/20250702080756_BumpingRequest.cs similarity index 96% rename from src/Migrations/20250626132159_AddBumpingRequest.cs rename to src/Migrations/20250702080756_BumpingRequest.cs index bbcfaf93..b8453a9e 100644 --- a/src/Migrations/20250626132159_AddBumpingRequest.cs +++ b/src/Migrations/20250702080756_BumpingRequest.cs @@ -5,7 +5,7 @@ namespace NodeGuard.Migrations { /// - public partial class AddBumpingRequest : Migration + public partial class BumpingRequest : Migration { /// protected override void Up(MigrationBuilder migrationBuilder) diff --git a/src/Pages/Withdrawals.razor b/src/Pages/Withdrawals.razor index 48ec6a95..f388688b 100644 --- a/src/Pages/Withdrawals.razor +++ b/src/Pages/Withdrawals.razor @@ -1151,8 +1151,10 @@ throw new Exception(); // To-Do }); _selectedUTXOs = await CoinSelectionService.GetUTXOsByOutpointAsync(originalRequest.Wallet.GetDerivationStrategy(), outpoints); + originalRequest.Status = WalletWithdrawalRequestStatus.Bumped; WalletWithdrawalRequestRepository.Update(bumpedRequest); + WalletWithdrawalRequestRepository.Update(originalRequest); } if (_selectedUTXOs.Count > 0) diff --git a/src/Proto/nodeguard.proto b/src/Proto/nodeguard.proto index fa69694a..6bb2f72f 100644 --- a/src/Proto/nodeguard.proto +++ b/src/Proto/nodeguard.proto @@ -376,6 +376,7 @@ enum WITHDRAWAL_REQUEST_STATUS { WITHDRAWAL_REJECTED = 3; WITHDRAWAL_PENDING_CONFIRMATION = 4; WITHDRAWAL_FAILED = 5; + WITHDRAWAL_BUMPED = 6; } message GetWithdrawalsRequestStatusRequest { diff --git a/src/Rpc/NodeGuardService.cs b/src/Rpc/NodeGuardService.cs index e95ea788..218ecfeb 100644 --- a/src/Rpc/NodeGuardService.cs +++ b/src/Rpc/NodeGuardService.cs @@ -1029,6 +1029,7 @@ private WITHDRAWAL_REQUEST_STATUS GetStatus(WalletWithdrawalRequestStatus status WalletWithdrawalRequestStatus.Pending => WITHDRAWAL_REQUEST_STATUS.WithdrawalPendingApproval, WalletWithdrawalRequestStatus.PSBTSignaturesPending => WITHDRAWAL_REQUEST_STATUS.WithdrawalPendingApproval, WalletWithdrawalRequestStatus.Rejected => WITHDRAWAL_REQUEST_STATUS.WithdrawalRejected, + WalletWithdrawalRequestStatus.Bumped => WITHDRAWAL_REQUEST_STATUS.WithdrawalBumped, _ => throw new ArgumentOutOfRangeException(nameof(status), status, "Unknown status") }; } From d056df3339a15b0bf4e33d28f0b4d887017b1ae2 Mon Sep 17 00:00:00 2001 From: Marcos Date: Mon, 7 Jul 2025 12:24:44 +0200 Subject: [PATCH 21/49] refactor: specify function for wds while also including the wallet wd req in the utxo --- src/Data/Repositories/FUTXORepository.cs | 29 +++++-------------- .../Interfaces/IFMUTXORepository.cs | 2 +- 2 files changed, 9 insertions(+), 22 deletions(-) diff --git a/src/Data/Repositories/FUTXORepository.cs b/src/Data/Repositories/FUTXORepository.cs index 2b6c1c15..17dcb213 100644 --- a/src/Data/Repositories/FUTXORepository.cs +++ b/src/Data/Repositories/FUTXORepository.cs @@ -91,30 +91,17 @@ public async Task> GetAll() return _repository.Update(type, applicationDbContext); } - public async Task> GetLockedUTXOsByRequestId(int? walletWithdrawalRequestId = null, int? channelOperationRequestId = null) + public async Task> GetLockedUTXOsByWithdrawalId(int walletWithdrawalRequestId) { await using var applicationDbContext = await _dbContextFactory.CreateDbContextAsync(); - var walletWithdrawalRequestsLockedUTXOs = new List(); - if (walletWithdrawalRequestId != null) - { - walletWithdrawalRequestsLockedUTXOs = await applicationDbContext.WalletWithdrawalRequests - .Include(x => x.UTXOs) - .Where(x => x.Id == walletWithdrawalRequestId) - .SelectMany(x => x.UTXOs).ToListAsync(); - } - - var channelOperationRequestsLockedUTXOs = new List(); - - if (channelOperationRequestId != null) - { - walletWithdrawalRequestsLockedUTXOs = await applicationDbContext.ChannelOperationRequests - .Include(x => x.Utxos) - .Where(x => x.Id == walletWithdrawalRequestId) - .SelectMany(x => x.Utxos).ToListAsync(); - } - - var result = walletWithdrawalRequestsLockedUTXOs.Union(channelOperationRequestsLockedUTXOs).ToList(); + var result = new List(); + result = await applicationDbContext.WalletWithdrawalRequests + .Include(x => x.UTXOs) + .Where(x => x.Id == walletWithdrawalRequestId) + .SelectMany(x => x.UTXOs) + .Include(x => x.WalletWithdrawalRequests) + .ToListAsync(); return result; } diff --git a/src/Data/Repositories/Interfaces/IFMUTXORepository.cs b/src/Data/Repositories/Interfaces/IFMUTXORepository.cs index ef78613a..83c31a5e 100644 --- a/src/Data/Repositories/Interfaces/IFMUTXORepository.cs +++ b/src/Data/Repositories/Interfaces/IFMUTXORepository.cs @@ -46,5 +46,5 @@ public interface IFMUTXORepository /// Gets the current list of UTXOs locked on requests ChannelOperationRequest / WalletWithdrawalRequest by passing its id /// /// - Task> GetLockedUTXOsByRequestId(int? walletWithdrawalRequestId = null, int? channelOperationRequestId = null); + Task> GetLockedUTXOsByWithdrawalId(int walletWithdrawalRequestId); } \ No newline at end of file From 07aa9bec1bd3a780de6336082f7804aa7ec9f61c Mon Sep 17 00:00:00 2001 From: scrpti Date: Mon, 7 Jul 2025 13:52:10 +0200 Subject: [PATCH 22/49] remove unused variable for changeless amount in BitcoinService --- src/Pages/Withdrawals.razor | 72 ++++++++++++++++++++++------------ src/Services/BitcoinService.cs | 4 +- 2 files changed, 48 insertions(+), 28 deletions(-) diff --git a/src/Pages/Withdrawals.razor b/src/Pages/Withdrawals.razor index f388688b..0efe15d3 100644 --- a/src/Pages/Withdrawals.razor +++ b/src/Pages/Withdrawals.razor @@ -817,13 +817,12 @@ return; } - //If its changeless show a toast - if (walletWithdrawalRequest.Changeless) + @* if (walletWithdrawalRequest.Changeless) { ToastService.ShowError("Fee bumping is not supported for changeless transactions. Please create a new withdrawal with higher fees instead."); return; - } - + } *@ + try { _selectedRequest = walletWithdrawalRequest; @@ -976,27 +975,48 @@ }; newRequest.RequestMetadata = JsonConvert.SerializeObject(metadata); } - if(newRequest.Changeless) - { - /* - * CHANGELESS FEE BUMPING IS NOT SUPPORTED - * - * Previous attempts to implement changeless fee bumping have failed due to: - * 1. Double fee calculation issues - the UI calculates fees and NBitcoin also calculates fees, - * leading to "NotEnoughFundsException" when the total exceeds available input value. - * 2. Precision mismatches between decimal calculations in the UI and NBitcoin's satoshi-based calculations. - * 3. Complex UTXO selection and validation logic that doesn't account for changeless scenarios properly. - * 4. NBitcoin's transaction builder expecting standard fee estimation patterns, not explicit fee amounts. - * - * Attempted solutions that didn't work: - * - Calculating fees in satoshis for precision matching - * - Explicitly setting fees with builder.SendFees() - * - Validating UTXO availability with unconfirmed UTXOs - * - Custom fee calculation logic for changeless transactions - * - * For now, changeless transactions cannot be fee-bumped. Users should create a new - * transaction with higher fees instead. - */ + if(newRequest.Changeless){ + //Si es changeless cambiaremos el output a output = output - diffFee donde diffFee es la diferencia entre el fee actual y el nuevo fee + var txInfo = await NBXplorerService.GetTransactionAsync(uint256.Parse(_selectedRequest.TxId)); + var tx = txInfo.Transaction; + long vSize = tx.GetVirtualSize(); + decimal oldFeeRate; + + if (_selectedRequest.MempoolRecommendedFeesType == MempoolRecommendedFeesType.CustomFee) + { + if (!_selectedRequest.CustomFeeRate.HasValue) + { + ToastService.ShowError("Custom fee rate not found."); + return; + } + + oldFeeRate = _selectedRequest.CustomFeeRate.Value; + } + else + { + var fetchedRate = await NBXplorerService.GetFeesByType(_selectedRequest.MempoolRecommendedFeesType); + if (!fetchedRate.HasValue) + { + ToastService.ShowError("Could not fetch original mempool fee rate."); + return; + } + + oldFeeRate = fetchedRate.Value; + } + + decimal oldFee = (vSize * oldFeeRate) / 100_000_000m; + decimal newFee = (vSize * newFeeRate) / 100_000_000m; + decimal feeDiff = newFee - oldFee; + + if (newRequest.Amount <= feeDiff) + { + ToastService.ShowError("Cannot bump fee: the increased fee exceeds the amount being sent."); + return; + } + + newRequest.Amount -= feeDiff; + ToastService.ShowInfo($"Changeless bump: reduced output by {feeDiff:f8} BTC to increase the fee."); + } await Bumpfee(newRequest); await _bumpfeeRef.HideModal(); @@ -1134,7 +1154,7 @@ if (_selectedRequest.BumpingId != null) { WalletWithdrawalRequest originalRequest = await WalletWithdrawalRequestRepository.GetById((int)_selectedRequest.BumpingId); - List mUTXOs = await FMUTXORepository.GetLockedUTXOsByRequestId(originalRequest.Id); + List mUTXOs = await FMUTXORepository.GetLockedUTXOsByWithdrawalId(originalRequest.Id); WalletWithdrawalRequest bumpedRequest = await WalletWithdrawalRequestRepository.GetById(_selectedRequest.Id); if (bumpedRequest.UTXOs == null) { diff --git a/src/Services/BitcoinService.cs b/src/Services/BitcoinService.cs index 67a3a155..c96d3bc9 100644 --- a/src/Services/BitcoinService.cs +++ b/src/Services/BitcoinService.cs @@ -217,7 +217,7 @@ await _coinSelectionService.GetTxInputCoins(availableUTXOs, walletWithdrawalRequ var builder = txBuilder; builder.AddCoins(scriptCoins); - var changelessAmount = selectedUTXOs.Sum(u => (Money)u.Value); + var amount = new Money(walletWithdrawalRequest.SatsAmount, MoneyUnit.Satoshi); var destination = BitcoinAddress.Create(walletWithdrawalRequest.DestinationAddress, nbXplorerNetwork); @@ -236,7 +236,7 @@ await _coinSelectionService.GetTxInputCoins(availableUTXOs, walletWithdrawalRequ } else { - builder.Send(destination, walletWithdrawalRequest.Changeless ? changelessAmount : amount); + builder.Send(destination, amount); if (walletWithdrawalRequest.Changeless) { builder.SubtractFees(); From 9dfd2c863c22744e9fb3f66a7f91bb0f58137d4a Mon Sep 17 00:00:00 2001 From: scrpti Date: Mon, 7 Jul 2025 17:38:37 +0200 Subject: [PATCH 23/49] feat: changeless transactions bump fee method is available --- src/Pages/Withdrawals.razor | 34 ++++++---------------------------- 1 file changed, 6 insertions(+), 28 deletions(-) diff --git a/src/Pages/Withdrawals.razor b/src/Pages/Withdrawals.razor index 0efe15d3..ad4ccffe 100644 --- a/src/Pages/Withdrawals.razor +++ b/src/Pages/Withdrawals.razor @@ -378,20 +378,6 @@ } - - - @{ - if (!string.IsNullOrEmpty(context.RequestMetadata)) - { - var metadata = JsonConvert.DeserializeObject>(context.RequestMetadata); - if (metadata != null && metadata.TryGetValue("bumpOfTxId", out var parentTxId)) - { - Original tx - } - } - } - - @if (_isFinanceManager && context.Status == WalletWithdrawalRequestStatus.OnChainConfirmationPending) @@ -542,7 +528,6 @@ public static readonly ColumnDefault UpdateDate = new("Update Date"); public static readonly ColumnDefault Links = new("Links"); public static readonly ColumnDefault Actions = new("Actions"); - public static readonly ColumnDefault ParentTx = new("Parent Tx"); } protected override async Task OnInitializedAsync() @@ -966,14 +951,6 @@ // Use the recommended fee rate from NBXplorer service based on the selected mempool fee type var recommendedFeeRate = await GetRecommendedFeeRate(newRequest.MempoolRecommendedFeesType); newFeeRate = (decimal)recommendedFeeRate; - } - if (!string.IsNullOrEmpty(_selectedRequest?.TxId)) - { - var metadata = new Dictionary - { - { "bumpOfTxId", _selectedRequest.TxId } - }; - newRequest.RequestMetadata = JsonConvert.SerializeObject(metadata); } if(newRequest.Changeless){ //Si es changeless cambiaremos el output a output = output - diffFee donde diffFee es la diferencia entre el fee actual y el nuevo fee @@ -1014,7 +991,7 @@ return; } - newRequest.Amount -= feeDiff; + ToastService.ShowInfo($"Changeless bump: reduced output by {feeDiff:f8} BTC to increase the fee."); } @@ -1138,20 +1115,18 @@ if (_selectedRequest != null) { try - { + { _selectedRequest.Wallet = null; - _selectedRequest.Changeless = _selectedUTXOs.Count > 0; _selectedRequest.WithdrawAllFunds = _isCheckedAllFunds || _amount == _selectedRequestWalletBalance; _selectedRequest.MempoolRecommendedFeesType = _selectedMempoolRecommendedFeesType; _selectedRequest.CustomFeeRate = _customSatPerVbAmount; + var createWithdrawalResult = await WalletWithdrawalRequestRepository.AddAsync(_selectedRequest); if (!createWithdrawalResult.Item1) { throw new ShowToUserException(createWithdrawalResult.Item2); } - ToastService.ShowSuccess("Withdrawal request created!"); - if (_selectedRequest.BumpingId != null) { WalletWithdrawalRequest originalRequest = await WalletWithdrawalRequestRepository.GetById((int)_selectedRequest.BumpingId); List mUTXOs = await FMUTXORepository.GetLockedUTXOsByWithdrawalId(originalRequest.Id); @@ -1176,6 +1151,9 @@ WalletWithdrawalRequestRepository.Update(bumpedRequest); WalletWithdrawalRequestRepository.Update(originalRequest); } + _selectedRequest.Changeless = _selectedUTXOs.Count > 0; + + ToastService.ShowSuccess("Withdrawal request created!"); if (_selectedUTXOs.Count > 0) { From 5742520082f3bfb909202b251f84ee3749b4cf39 Mon Sep 17 00:00:00 2001 From: Marcos Date: Mon, 7 Jul 2025 17:47:40 +0200 Subject: [PATCH 24/49] chore: add mempool space to docker compose --- docker/docker-compose.yml | 84 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 13021b8a..33ca4b80 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -61,9 +61,93 @@ services: timeout: 5s retries: 5 + mempool-frontend-btc: + profiles: ["mempool-btc"] + environment: + FRONTEND_HTTP_PORT: "8080" + BACKEND_MAINNET_HTTP_HOST: "mempool-backend-btc" + LIQUID_ENABLED: false + LIQUID_TESTNET_ENABLED: false + image: mempool/frontend:latest + container_name: 40swap_mempool_frontend_btc + user: "1000:1000" + restart: always + command: "./wait-for mempool-db-btc:3306 --timeout=720 -- nginx -g 'daemon off;'" + ports: + - 7084:8080 + + mempool-backend-btc: + profiles: ["mempool-btc"] + environment: + MEMPOOL_BACKEND: "electrum" + CORE_RPC_HOST: "host.docker.internal" + CORE_RPC_PORT: "18443" + CORE_RPC_USERNAME: "polaruser" + CORE_RPC_PASSWORD: "polarpass" + DATABASE_ENABLED: "true" + DATABASE_HOST: "mempool-db-btc" + DATABASE_DATABASE: "mempool_btc" + DATABASE_USERNAME: "mempool" + DATABASE_PASSWORD: "mempool" + STATISTICS_ENABLED: "true" + ELECTRUM_HOST: "electrumx" + ELECTRUM_PORT: "50001" + ELECTRUM_TLS_ENABLED: "false" + image: mempool/backend:latest + container_name: 40swap_mempool_backend_btc + user: "1000:1000" + restart: always + command: "./wait-for-it.sh mempool-db-btc:3306 --timeout=720 --strict -- ./start.sh" + depends_on: + - mempool-db-btc + volumes: + - mempool-backend-btc-data:/backend/cache + + mempool-db-btc: + profiles: ["mempool-btc"] + environment: + MYSQL_DATABASE: "mempool_btc" + MYSQL_USER: "mempool" + MYSQL_PASSWORD: "mempool" + MYSQL_ROOT_PASSWORD: "admin" + image: mariadb:10.5.8 + container_name: 40swap_mempool_db_btc + restart: always + volumes: + - mempool-db-btc-data:/var/lib/mysql + + electrumx: + profiles: ["mempool-btc"] + image: andgohq/electrumx:1.8.7 + container_name: 40swap_electrumx + command: ["wait-for-it.sh", "host.docker.internal:18443", "--", "init"] + ports: + - "51002:50002" + - "51001:50001" + expose: + - "50001" + - "50002" + volumes: + - electrumx-data:/data + environment: + # bitcoind is valid + - DAEMON_URL=http://polaruser:polarpass@host.docker.internal:18443 + - COIN=BitcoinSegwit + - NET=regtest + # 127.0.0.1 or electrumx is valid for RPC_HOST + - RPC_HOST=electrumx + - RPC_PORT=18443 + - HOST=electrumx + - TCP_PORT=50001 + - SSL_PORT=50002 + restart: always + volumes: nodeguard_postgres_data: bitcoin_datadir: nbxplorer_datadir: nbxplorer_postgres_data: nodeguard_data_keys_dir: + mempool-backend-btc-data: + mempool-db-btc-data: + electrumx-data: From dfd4e03a2adaf6f147af01497f6b514f88e2770e Mon Sep 17 00:00:00 2001 From: Marcos Date: Mon, 21 Jul 2025 13:20:57 +0200 Subject: [PATCH 25/49] fix: bad handling of the vars --- src/Shared/BumpfeeModal.razor | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Shared/BumpfeeModal.razor b/src/Shared/BumpfeeModal.razor index 765c44e9..69cc92b7 100644 --- a/src/Shared/BumpfeeModal.razor +++ b/src/Shared/BumpfeeModal.razor @@ -99,9 +99,9 @@ private async Task HandleOnClick() { - long _currentSatsPerVbAmount = (long?)await NBXplorerService.GetFeesByType(WithdrawalRequest.MempoolRecommendedFeesType) ?? 1; - if (_customSatPerVbAmount <= _currentSatsPerVbAmount) { - ToastService.ShowError($"Fee must be greater than the current one ({_currentSatsPerVbAmount} sats/vB)"); + var oldFee = WithdrawalRequest?.CustomFeeRate ?? 0; + if (_customSatPerVbAmount <= oldFee) { + ToastService.ShowError($"Fee must be greater than the current one ({oldFee} sats/vB)"); } else { WalletWithdrawalRequest newRequest = new WalletWithdrawalRequest(); newRequest.Description = WithdrawalRequest.Description; From 84481acaf77eaabf4de10baf3b07c8f4ecda5fda Mon Sep 17 00:00:00 2001 From: Marcos Date: Mon, 21 Jul 2025 17:10:20 +0200 Subject: [PATCH 26/49] fix: fixing migrations --- ...1807_UpdateSourceDestChannelNodeIdFromRequests.cs | 6 +----- .../20250611142943_MigrateLegacyWithdrawalData.cs | 12 +++++------- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/src/Migrations/20230309121807_UpdateSourceDestChannelNodeIdFromRequests.cs b/src/Migrations/20230309121807_UpdateSourceDestChannelNodeIdFromRequests.cs index 3dedb3bd..53719ef8 100644 --- a/src/Migrations/20230309121807_UpdateSourceDestChannelNodeIdFromRequests.cs +++ b/src/Migrations/20230309121807_UpdateSourceDestChannelNodeIdFromRequests.cs @@ -31,12 +31,8 @@ ORDER BY ""CreationDatetime"" protected override void Down(MigrationBuilder migrationBuilder) { - string query = @" -BEGIN TRANSACTION; - UPDATE public.""Channels"" SET ""SourceNodeId"" = 1, ""DestinationNodeId"" = 1; -COMMIT;"; + string query = @"UPDATE public.""Channels"" SET ""SourceNodeId"" = 1, ""DestinationNodeId"" = 1;"; migrationBuilder.Sql(query); - } } } diff --git a/src/Migrations/20250611142943_MigrateLegacyWithdrawalData.cs b/src/Migrations/20250611142943_MigrateLegacyWithdrawalData.cs index 9346fd96..12615403 100644 --- a/src/Migrations/20250611142943_MigrateLegacyWithdrawalData.cs +++ b/src/Migrations/20250611142943_MigrateLegacyWithdrawalData.cs @@ -10,10 +10,8 @@ public partial class MigrateLegacyWithdrawalData : Migration /// protected override void Up(MigrationBuilder migrationBuilder) { - // Execute data migration in an explicit transaction + // Execute data migration migrationBuilder.Sql(@" - BEGIN; - -- Step 1: Migrate existing data from Amount and DestinationAddress to WalletWithdrawalRequestDestinations INSERT INTO ""WalletWithdrawalRequestDestinations"" (""Amount"", ""Address"", ""WalletWithdrawalRequestId"", ""CreationDatetime"", ""UpdateDatetime"") SELECT @@ -24,8 +22,10 @@ protected override void Up(MigrationBuilder migrationBuilder) ""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 + "); + + // Step 2: Verify migration was successful + migrationBuilder.Sql(@" DO $$ DECLARE original_count INTEGER; @@ -50,8 +50,6 @@ SELECT COUNT(*) INTO migrated_count -- Log success RAISE NOTICE 'Successfully migrated % legacy withdrawal records to destinations table', original_count; END $$; - - COMMIT; "); } From 3b9939c364cb62f4ee66a872b38d89a80b8ba77e Mon Sep 17 00:00:00 2001 From: Marcos Date: Tue, 22 Jul 2025 13:05:27 +0200 Subject: [PATCH 27/49] fix: when a bumping tx fails update the other one to onchainconfpending again and dont allow bump fee if fee+amount is more than utxo --- src/Pages/Withdrawals.razor | 68 +++++++++++++++++++++++++++---------- 1 file changed, 51 insertions(+), 17 deletions(-) diff --git a/src/Pages/Withdrawals.razor b/src/Pages/Withdrawals.razor index 37b30288..821e07f9 100644 --- a/src/Pages/Withdrawals.razor +++ b/src/Pages/Withdrawals.razor @@ -969,26 +969,40 @@ var recommendedFeeRate = await GetRecommendedFeeRate(newRequest.MempoolRecommendedFeesType); newFeeRate = (decimal)recommendedFeeRate; } - if(newRequest.Changeless){ - //Si es changeless cambiaremos el output a output = output - diffFee donde diffFee es la diferencia entre el fee actual y el nuevo fee - var txInfo = await NBXplorerService.GetTransactionAsync(uint256.Parse(_selectedRequest.TxId)); - var tx = txInfo.Transaction; - long vSize = tx.GetVirtualSize(); - decimal oldFeeRate = _selectedRequest.CustomFeeRate ?? 1; - - decimal oldFee = (vSize * oldFeeRate) / 100_000_000m; - decimal newFee = (vSize * newFeeRate) / 100_000_000m; - decimal feeDiff = newFee - oldFee; - - var amount = newRequest.WalletWithdrawalRequestDestinations.Sum(x => x.Amount); - if (amount <= feeDiff) + + // If the new fee plus the amount exceeds the utxo sum value, show an error + // NOTE: revisit this when full rbf because we could add a UTXO + var txInfo = await NBXplorerService.GetTransactionAsync(uint256.Parse(_selectedRequest.TxId)); + var tx = txInfo.Transaction; + long vSize = tx.GetVirtualSize(); + decimal newFee = (vSize * newFeeRate) / 100_000_000m; + + // Retrieve from database to get the UTXOs + _selectedRequest = await WalletWithdrawalRequestRepository.GetById(_selectedRequest.Id); + + if (_selectedRequest == null) + { + ToastService.ShowError("Withdrawal request not found"); + return; + } + + if (_selectedRequest.UTXOs != null && _selectedRequest.UTXOs.Count > 0) + { + var inputAmount = Money.Satoshis(_selectedRequest.UTXOs.Sum(x => x.SatsAmount)).ToDecimal(MoneyUnit.BTC); // Convert to BTC + if (inputAmount <= newFee + _selectedRequest.TotalAmount) { - ToastService.ShowError("Cannot bump fee: the increased fee exceeds the amount being sent."); + ToastService.ShowError("The new fee plus the amount exceeds the sum of the selected UTXOs. Please lower the fee rate."); return; } - - ToastService.ShowInfo($"Changeless bump: reduced output by {feeDiff:f8} BTC to increase the fee."); } + + // If the request is changeless, show an error + // NOTE: Revisit this when full RBF is implemented because we could add a UTXO + if (newRequest.Changeless){ + ToastService.ShowError("Fee bumping is not supported for changeless transactions. Please create a new withdrawal with higher fees instead."); + return; + } + await Bumpfee(newRequest); await _bumpfeeRef.HideModal(); } else { @@ -1026,7 +1040,6 @@ ToastService.ShowError("Something went wrong"); } - await HideRejectCancelModal(); } } @@ -1220,14 +1233,35 @@ catch (NoUTXOsAvailableException e) { CleanUp("No UTXOs available for withdrawals were found for this wallet"); + await ResetStatusBumpedIfError(_selectedRequest); } catch (ShowToUserException e) { CleanUp(e.Message); + await ResetStatusBumpedIfError(_selectedRequest); } catch { CleanUp("Something went wrong"); + await ResetStatusBumpedIfError(_selectedRequest); + } + } + } + + private async Task ResetStatusBumpedIfError(WalletWithdrawalRequest request) + { + if (request.BumpingId != null) + { + var bumpedRequest = await WalletWithdrawalRequestRepository.GetById(request.BumpingId.Value); + + if (bumpedRequest != null) + { + bumpedRequest.Status = WalletWithdrawalRequestStatus.OnChainConfirmationPending; + var updateResult = WalletWithdrawalRequestRepository.Update(bumpedRequest); + if (!updateResult.Item1) + { + ToastService.ShowError("Error while updating the bumped request, please contact a superadmin for troubleshooting"); + } } } } From 7ca75d1ef319f75f081b9763b22dfe42beb18e21 Mon Sep 17 00:00:00 2001 From: Marcos Date: Wed, 23 Jul 2025 10:22:43 +0200 Subject: [PATCH 28/49] fix: remove unused imports --- src/Shared/BumpfeeModal.razor | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Shared/BumpfeeModal.razor b/src/Shared/BumpfeeModal.razor index 69cc92b7..7301c160 100644 --- a/src/Shared/BumpfeeModal.razor +++ b/src/Shared/BumpfeeModal.razor @@ -1,8 +1,5 @@ @using NBitcoin -@using Microsoft.CodeAnalysis.Text -@using System.Xml.Schema @using JSException = Microsoft.JSInterop.JSException -@using System.Runtime.InteropServices.JavaScript @if (WithdrawalRequest != null) { From 9e293d0559d1cb69b8727033167937b620a4f6b5 Mon Sep 17 00:00:00 2001 From: Marcos Date: Wed, 23 Jul 2025 11:23:12 +0200 Subject: [PATCH 29/49] fix: if formats and remove todo --- src/Pages/Withdrawals.razor | 36 +++++++++++++++++++++++------------ src/Shared/BumpfeeModal.razor | 10 +++++++--- 2 files changed, 31 insertions(+), 15 deletions(-) diff --git a/src/Pages/Withdrawals.razor b/src/Pages/Withdrawals.razor index 821e07f9..d9224a48 100644 --- a/src/Pages/Withdrawals.razor +++ b/src/Pages/Withdrawals.razor @@ -799,11 +799,12 @@ private async Task ShowBumpfeeModal(WalletWithdrawalRequest walletWithdrawalRequest) { ulong confirmations = 0; - if (!string.IsNullOrEmpty(walletWithdrawalRequest.TxId)) { + if (!string.IsNullOrEmpty(walletWithdrawalRequest.TxId)) + { try { var nbxplorerStatus = await NBXplorerService.GetTransactionAsync(uint256.Parse(walletWithdrawalRequest.TxId)); - confirmations = (ulong)(nbxplorerStatus?.Confirmations ?? 0); + confirmations = (ulong)(nbxplorerStatus?.Confirmations ?? throw new Exception("Failed to get confirmations")); } catch (Exception) { @@ -819,13 +820,16 @@ return; } - @* if (walletWithdrawalRequest.Changeless) + + // NOTE: Revisit this when full RBF is implemented because we could add a UTXO + if (walletWithdrawalRequest.Changeless) { ToastService.ShowError("Fee bumping is not supported for changeless transactions. Please create a new withdrawal with higher fees instead."); return; - } *@ + } - try { + try + { _selectedRequest = walletWithdrawalRequest; if (_bumpfeeRef != null) await _bumpfeeRef.ShowModal(); @@ -944,16 +948,20 @@ private async Task Bumpfee(WalletWithdrawalRequest request) { - try { + try + { await CreateNewWithdrawal(request); - } catch { + } + catch + { ToastService.ShowError("Could not bump the withdrawal"); } } private async void SubmitBumpfeeModal(WalletWithdrawalRequest newRequest) { - if (newRequest != null && _bumpfeeRef != null) { + if (newRequest != null && _bumpfeeRef != null) + { newRequest.WalletId = _selectedRequest.WalletId; newRequest.Wallet = await WalletRepository.GetById(newRequest.WalletId); @@ -998,14 +1006,17 @@ // If the request is changeless, show an error // NOTE: Revisit this when full RBF is implemented because we could add a UTXO - if (newRequest.Changeless){ + if (newRequest.Changeless) + { ToastService.ShowError("Fee bumping is not supported for changeless transactions. Please create a new withdrawal with higher fees instead."); return; } await Bumpfee(newRequest); await _bumpfeeRef.HideModal(); - } else { + } + else + { ToastService.ShowError("Something went wrong"); } } @@ -1134,7 +1145,8 @@ throw new ShowToUserException(createWithdrawalResult.Item2); } - if (_selectedRequest.BumpingId != null) { + if (_selectedRequest.BumpingId != null) + { WalletWithdrawalRequest originalRequest = await WalletWithdrawalRequestRepository.GetById((int)_selectedRequest.BumpingId); if (originalRequest == null) { @@ -1169,7 +1181,7 @@ if (OutPoint.TryParse($"{u.TxId}:{u.OutputIndex}", out o)) outpoints.Add(o); else - throw new Exception(); // To-Do + throw new FormatException("Failed to parse OutPoint from TxId and OutputIndex"); }); _selectedUTXOs = await CoinSelectionService.GetUTXOsByOutpointAsync(originalRequest.Wallet.GetDerivationStrategy(), outpoints); originalRequest.Status = WalletWithdrawalRequestStatus.Bumped; diff --git a/src/Shared/BumpfeeModal.razor b/src/Shared/BumpfeeModal.razor index 7301c160..1e80ec40 100644 --- a/src/Shared/BumpfeeModal.razor +++ b/src/Shared/BumpfeeModal.razor @@ -97,9 +97,12 @@ private async Task HandleOnClick() { var oldFee = WithdrawalRequest?.CustomFeeRate ?? 0; - if (_customSatPerVbAmount <= oldFee) { + if (_customSatPerVbAmount <= oldFee) + { ToastService.ShowError($"Fee must be greater than the current one ({oldFee} sats/vB)"); - } else { + } + else + { WalletWithdrawalRequest newRequest = new WalletWithdrawalRequest(); newRequest.Description = WithdrawalRequest.Description; newRequest.Changeless = WithdrawalRequest.Changeless; @@ -115,7 +118,8 @@ public async Task ShowModal() { var walletId = WithdrawalRequest?.WalletId ?? -1; - if (walletId != -1) { + if (walletId != -1) + { var wallet = await WalletRepository.GetById(walletId); var (balance, _) = await BitcoinService.GetWalletConfirmedBalance(wallet); _selectedWalletBalance = balance; From b4800ada8a86808d0dbda23ee540a59166eba851 Mon Sep 17 00:00:00 2001 From: Marcos Date: Wed, 23 Jul 2025 11:34:54 +0200 Subject: [PATCH 30/49] feat: dont return dust --- src/Helpers/Constants.cs | 3 ++- src/Pages/Withdrawals.razor | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Helpers/Constants.cs b/src/Helpers/Constants.cs index 05f6f2f4..d045b045 100644 --- a/src/Helpers/Constants.cs +++ b/src/Helpers/Constants.cs @@ -75,6 +75,7 @@ public class Constants public static readonly long MINIMUM_SWEEP_TRANSACTION_AMOUNT_SATS = 25_000_000; //25M sats public static readonly string DEFAULT_DERIVATION_PATH = "48'/1'"; public static readonly int SESSION_TIMEOUT_MILLISECONDS = 3_600_000; + public static readonly decimal BITCOIN_DUST = 0.00000546m; // 546 satoshi in BTC //Sat/vb ratio public static decimal MIN_SAT_PER_VB_RATIO = 0.9m; @@ -100,7 +101,7 @@ public class Constants public static readonly string ALICE_PUBKEY = "02dc2ae598a02fc1e9709a23b68cd51d7fa14b1132295a4d75aa4f5acd23ee9527"; public static readonly string ALICE_HOST = "host.docker.internal:10001"; public static readonly string ALICE_MACAROON = "0201036c6e6402f801030a108cdfeb2614b8335c11aebb358f888d6d1201301a160a0761646472657373120472656164120577726974651a130a04696e666f120472656164120577726974651a170a08696e766f69636573120472656164120577726974651a210a086d616361726f6f6e120867656e6572617465120472656164120577726974651a160a076d657373616765120472656164120577726974651a170a086f6666636861696e120472656164120577726974651a160a076f6e636861696e120472656164120577726974651a140a057065657273120472656164120577726974651a180a067369676e6572120867656e657261746512047265616400000620c999e1a30842cbae3f79bd633b19d5ec0d2b6ebdc4880f6f5d5c230ce38f26ab"; - public static readonly string BOB_PUBKEY = "038644c6b13cdfc59bc97c2cc2b1418ced78f6d01da94f3bfd5fdf8b197335ea84"; + public static readonly string BOB_PUBKEY = "038644c6b13cdfc59bc97c2cc2b1418ced78f6d01da94f3bfd5fdf8b197335ea84"; public static readonly string BOB_HOST = "host.docker.internal:10002"; public static readonly string BOB_MACAROON = "0201036c6e6402f801030a10e0e89a68f9e2398228a995890637d2531201301a160a0761646472657373120472656164120577726974651a130a04696e666f120472656164120577726974651a170a08696e766f69636573120472656164120577726974651a210a086d616361726f6f6e120867656e6572617465120472656164120577726974651a160a076d657373616765120472656164120577726974651a170a086f6666636861696e120472656164120577726974651a160a076f6e636861696e120472656164120577726974651a140a057065657273120472656164120577726974651a180a067369676e6572120867656e657261746512047265616400000620b85ae6b693338987cd65eda60a24573e962301b2a91d8f7c5625650d6368751f"; public static readonly string CAROL_PUBKEY = "03650f49929d84d9a6d9b5a66235c603a1a0597dd609f7cd3b15052382cf9bb1b4"; diff --git a/src/Pages/Withdrawals.razor b/src/Pages/Withdrawals.razor index d9224a48..bb9baeb0 100644 --- a/src/Pages/Withdrawals.razor +++ b/src/Pages/Withdrawals.razor @@ -999,7 +999,7 @@ var inputAmount = Money.Satoshis(_selectedRequest.UTXOs.Sum(x => x.SatsAmount)).ToDecimal(MoneyUnit.BTC); // Convert to BTC if (inputAmount <= newFee + _selectedRequest.TotalAmount) { - ToastService.ShowError("The new fee plus the amount exceeds the sum of the selected UTXOs. Please lower the fee rate."); + ToastService.ShowError("The new fee plus the amount exceeds the sum of the selected UTXOs or returns dust. Please lower the fee rate."); return; } } From 95ed09c57fe8fae5534e8dd337e9cabb9fa90de8 Mon Sep 17 00:00:00 2001 From: Marcos Date: Wed, 23 Jul 2025 11:37:38 +0200 Subject: [PATCH 31/49] fix: return after error --- src/Pages/Withdrawals.razor | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Pages/Withdrawals.razor b/src/Pages/Withdrawals.razor index bb9baeb0..508c2ecc 100644 --- a/src/Pages/Withdrawals.razor +++ b/src/Pages/Withdrawals.razor @@ -997,7 +997,7 @@ if (_selectedRequest.UTXOs != null && _selectedRequest.UTXOs.Count > 0) { var inputAmount = Money.Satoshis(_selectedRequest.UTXOs.Sum(x => x.SatsAmount)).ToDecimal(MoneyUnit.BTC); // Convert to BTC - if (inputAmount <= newFee + _selectedRequest.TotalAmount) + if (inputAmount <= newFee + _selectedRequest.TotalAmount + Constants.BITCOIN_DUST) { ToastService.ShowError("The new fee plus the amount exceeds the sum of the selected UTXOs or returns dust. Please lower the fee rate."); return; @@ -1190,12 +1190,14 @@ if (!updateResult.Item1) { ToastService.ShowError("Something went wrong"); + return; } updateResult = WalletWithdrawalRequestRepository.Update(originalRequest); if (!updateResult.Item1) { ToastService.ShowError("Something went wrong"); + return; } } _selectedRequest.Changeless = _selectedUTXOs.Count > 0; From 6997448e7feefa6441d45afb971e944ae4ebf3dd Mon Sep 17 00:00:00 2001 From: Marcos Date: Wed, 23 Jul 2025 11:53:28 +0200 Subject: [PATCH 32/49] fix: exception --- src/Pages/Withdrawals.razor | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Pages/Withdrawals.razor b/src/Pages/Withdrawals.razor index 508c2ecc..82651e74 100644 --- a/src/Pages/Withdrawals.razor +++ b/src/Pages/Withdrawals.razor @@ -1315,7 +1315,7 @@ if (!string.IsNullOrEmpty(walletWithdrawalRequest.TxId)) { try { var nbxplorerStatus = await NBXplorerService.GetTransactionAsync(uint256.Parse(walletWithdrawalRequest.TxId)); - confirmations = (ulong)(nbxplorerStatus?.Confirmations ?? 0); + confirmations = (ulong)(nbxplorerStatus?.Confirmations ?? throw new Exception("Failed to get confirmations")); return walletWithdrawalRequest.Status == WalletWithdrawalRequestStatus.OnChainConfirmationPending && confirmations.Equals((ulong) 0); } catch (Exception) From 58c7af1d98620314c60dc9bb5d4da0f77cc1d819 Mon Sep 17 00:00:00 2001 From: Marcos Date: Wed, 23 Jul 2025 11:53:36 +0200 Subject: [PATCH 33/49] fix: nit --- src/Shared/BumpfeeModal.razor | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Shared/BumpfeeModal.razor b/src/Shared/BumpfeeModal.razor index 1e80ec40..987a131f 100644 --- a/src/Shared/BumpfeeModal.razor +++ b/src/Shared/BumpfeeModal.razor @@ -103,7 +103,7 @@ } else { - WalletWithdrawalRequest newRequest = new WalletWithdrawalRequest(); + var newRequest = new WalletWithdrawalRequest(); newRequest.Description = WithdrawalRequest.Description; newRequest.Changeless = WithdrawalRequest.Changeless; newRequest.WithdrawAllFunds = WithdrawalRequest.WithdrawAllFunds; From 2e90a9a3840de376497262dea9e4e91c0a65e314 Mon Sep 17 00:00:00 2001 From: Marcos Date: Wed, 23 Jul 2025 11:55:39 +0200 Subject: [PATCH 34/49] fix: remove dbtrie --- docker/docker-compose.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 2df2cc5f..287a0022 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -30,8 +30,6 @@ services: NBXPLORER_BIND: 0.0.0.0:32838 NBXPLORER_TRIMEVENTS: 10000 NBXPLORER_SIGNALFILESDIR: /datadir - #Keeping dbtrie for dev until it is fully removed since we would need to modify nbxplorer docker image to wait for the db to be ready - NBXPLORER_DBTRIE: 0 NBXPLORER_POSTGRES: Host=nbxplorer_postgres;Port=5432;Database=nbxplorer;Username=rw_dev;Password=rw_dev NBXPLORER_CHAINS: "btc" NBXPLORER_BTCRPCUSER: "polaruser" From e3aa62ebc6869db72c4456034857d691d058f1d7 Mon Sep 17 00:00:00 2001 From: Marcos Date: Wed, 23 Jul 2025 11:57:05 +0200 Subject: [PATCH 35/49] fix: remove dupl --- src/Pages/Withdrawals.razor | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Pages/Withdrawals.razor b/src/Pages/Withdrawals.razor index 82651e74..f89744e3 100644 --- a/src/Pages/Withdrawals.razor +++ b/src/Pages/Withdrawals.razor @@ -1170,10 +1170,6 @@ // Get utxos from the original request List mUTXOs = await FMUTXORepository.GetLockedUTXOsByWithdrawalId(originalRequest.Id); - if (_selectedRequest.UTXOs == null) { - _selectedRequest.UTXOs = new List(); - } - _selectedRequest.UTXOs = mUTXOs; List outpoints = new List(); mUTXOs.ForEach(u => { From dafda20f4e309519add7d7cffddd07ab0537161a Mon Sep 17 00:00:00 2001 From: Marcos Date: Wed, 23 Jul 2025 16:53:32 +0200 Subject: [PATCH 36/49] test: set rbf flag --- .../Services/BitcoinServiceTests.cs | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/test/NodeGuard.Tests/Services/BitcoinServiceTests.cs b/test/NodeGuard.Tests/Services/BitcoinServiceTests.cs index 9a51af49..5e5ebebc 100644 --- a/test/NodeGuard.Tests/Services/BitcoinServiceTests.cs +++ b/test/NodeGuard.Tests/Services/BitcoinServiceTests.cs @@ -201,7 +201,7 @@ async Task GenerateTemplatePSBT_LegacyMultiSigSucceeds() var result = await bitcoinService.GenerateTemplatePSBT(withdrawalRequest); // Assert - var psbt = PSBT.Parse("cHNidP8BAIkBAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/////wD/////AkBCDwAAAAAAIgAgPaPWaBQgTxHOMVfMfpX21blroUe8KAd6w2gLRelFuiAYUYkAAAAAACIAIDx3862ZOy+vKdDZ4oysyRZX0HARoqQ9LqqK2ukxoopiAAAAAE8BBDWHzwMvESQsgAAAAfw77kI6AYzrbSJqBmMojtD7XuD6nXkKs3DQMOBHMObIA4COLhzUgr3QcZaUPFqBM9Fpr4YCK2uwOBdxZE7AdETXEB/M5N4wAACAAQAAgAEAAIBPAQQ1h88DVqwD9IAAAAH5CK5KZrD/oasUtVrwzkjypwIly5AQkC1pAa+QuT6PgQJRrxXgW7i36sGJWz9fR//v7NgyGgLvIimPidCiA33wYBBg86CzMAAAgAEAAIABAACATwEENYfPA325Ro2AAAAB9SJwx2h6Ovs1HvTxuaMMEPO205IXBoOuqUiME5oRyZgDIiOFIzjqZ/v9jcNSqyYl55ondkYhI2vxwCEwkNNInp8Q7QIQyDAAAIABAACAAQAAgAABASuAlpgAAAAAACIAILNTGKQyViCBs/y3kcG+Q/3NcIIypkqLb3/EMmN57BDEAQVpUiEC2FTFYM/mwE4L60Q0G2p5QElV7YlMD7fcgoJEH79pLLEhAwJn/wsRl0hvcYj5Y3Bv3uQlxZ57pBZ9KSeuEPVNmjS/IQMaU3fyWsF+N0FpN8hSusDj6bESvd9YR509kdgWMLKLj1OuIgYC2FTFYM/mwE4L60Q0G2p5QElV7YlMD7fcgoJEH79pLLEYH8zk3jAAAIABAACAAQAAgAAAAAAAAAAAIgYDAmf/CxGXSG9xiPljcG/e5CXFnnukFn0pJ64Q9U2aNL8YYPOgszAAAIABAACAAQAAgAAAAAAAAAAAIgYDGlN38lrBfjdBaTfIUrrA4+mxEr3fWEedPZHYFjCyi48Y7QIQyDAAAIABAACAAQAAgAAAAAAAAAAAAAAA", Network.RegTest); + var psbt = PSBT.Parse("cHNidP8BAIkBAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/////wD9////AkBCDwAAAAAAIgAgPaPWaBQgTxHOMVfMfpX21blroUe8KAd6w2gLRelFuiAYUYkAAAAAACIAIDx3862ZOy+vKdDZ4oysyRZX0HARoqQ9LqqK2ukxoopiAAAAAE8BBDWHzwMvESQsgAAAAfw77kI6AYzrbSJqBmMojtD7XuD6nXkKs3DQMOBHMObIA4COLhzUgr3QcZaUPFqBM9Fpr4YCK2uwOBdxZE7AdETXEB/M5N4wAACAAQAAgAEAAIBPAQQ1h88DVqwD9IAAAAH5CK5KZrD/oasUtVrwzkjypwIly5AQkC1pAa+QuT6PgQJRrxXgW7i36sGJWz9fR//v7NgyGgLvIimPidCiA33wYBBg86CzMAAAgAEAAIABAACATwEENYfPA325Ro2AAAAB9SJwx2h6Ovs1HvTxuaMMEPO205IXBoOuqUiME5oRyZgDIiOFIzjqZ/v9jcNSqyYl55ondkYhI2vxwCEwkNNInp8Q7QIQyDAAAIABAACAAQAAgAABASuAlpgAAAAAACIAILNTGKQyViCBs/y3kcG+Q/3NcIIypkqLb3/EMmN57BDEAQVpUiEC2FTFYM/mwE4L60Q0G2p5QElV7YlMD7fcgoJEH79pLLEhAwJn/wsRl0hvcYj5Y3Bv3uQlxZ57pBZ9KSeuEPVNmjS/IQMaU3fyWsF+N0FpN8hSusDj6bESvd9YR509kdgWMLKLj1OuIgYC2FTFYM/mwE4L60Q0G2p5QElV7YlMD7fcgoJEH79pLLEYH8zk3jAAAIABAACAAQAAgAAAAAAAAAAAIgYDAmf/CxGXSG9xiPljcG/e5CXFnnukFn0pJ64Q9U2aNL8YYPOgszAAAIABAACAAQAAgAAAAAAAAAAAIgYDGlN38lrBfjdBaTfIUrrA4+mxEr3fWEedPZHYFjCyi48Y7QIQyDAAAIABAACAAQAAgAAAAAAAAAAAAAAA", Network.RegTest); result.Should().BeEquivalentTo(psbt); } @@ -266,7 +266,7 @@ async Task GenerateTemplatePSBT_MultiSigSucceeds() } }); fmutxoRepository - .Setup(x => x.GetLockedUTXOs(null , null)) + .Setup(x => x.GetLockedUTXOs(null, null)) .ReturnsAsync(new List()); utxoTagRepository .Setup(x => x.GetByKeyValue(It.IsAny(), It.IsAny())) @@ -280,7 +280,7 @@ async Task GenerateTemplatePSBT_MultiSigSucceeds() var result = await bitcoinService.GenerateTemplatePSBT(withdrawalRequest); // Assert - var psbt = PSBT.Parse("cHNidP8BAIkBAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/////wD/////AkBCDwAAAAAAIgAgPaPWaBQgTxHOMVfMfpX21blroUe8KAd6w2gLRelFuiAYUYkAAAAAACIAIDx3862ZOy+vKdDZ4oysyRZX0HARoqQ9LqqK2ukxoopiAAAAAE8BBDWHzwMvESQsgAAAAfw77kI6AYzrbSJqBmMojtD7XuD6nXkKs3DQMOBHMObIA4COLhzUgr3QcZaUPFqBM9Fpr4YCK2uwOBdxZE7AdETXEB/M5N4wAACAAQAAgAEAAIBPAQQ1h88DVqwD9IAAAAH5CK5KZrD/oasUtVrwzkjypwIly5AQkC1pAa+QuT6PgQJRrxXgW7i36sGJWz9fR//v7NgyGgLvIimPidCiA33wYBBg86CzMAAAgAEAAIABAACATwEENYfPA325Ro0AAAAAgN63GqLxTu1/NyL0SV4a0Hn1n8Dzg+Wye9nbb16ZISADr+s+pcKnDcSqKHKWSl4v8Rcq80ZqG/7QObYmZUl/xUYQ7QIQyDAAAIABAACAAAAAAAABASuAlpgAAAAAACIAINCp0IUCw4KZ8J/JokbAV1TBQtK4m6WLzUomP5VBhszOAQVpUiEC2FTFYM/mwE4L60Q0G2p5QElV7YlMD7fcgoJEH79pLLEhAwJn/wsRl0hvcYj5Y3Bv3uQlxZ57pBZ9KSeuEPVNmjS/IQNvzitZiz5ksZFSQuRibjPP4pwo+OWOqZLBL2x5ZrFVqVOuIgYC2FTFYM/mwE4L60Q0G2p5QElV7YlMD7fcgoJEH79pLLEYH8zk3jAAAIABAACAAQAAgAAAAAAAAAAAIgYDAmf/CxGXSG9xiPljcG/e5CXFnnukFn0pJ64Q9U2aNL8YYPOgszAAAIABAACAAQAAgAAAAAAAAAAAIgYDb84rWYs+ZLGRUkLkYm4zz+KcKPjljqmSwS9seWaxVakY7QIQyDAAAIABAACAAAAAAAAAAAAAAAAAAAAA", Network.RegTest); + var psbt = PSBT.Parse("cHNidP8BAIkBAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/////wD9////AkBCDwAAAAAAIgAgPaPWaBQgTxHOMVfMfpX21blroUe8KAd6w2gLRelFuiAYUYkAAAAAACIAIDx3862ZOy+vKdDZ4oysyRZX0HARoqQ9LqqK2ukxoopiAAAAAE8BBDWHzwMvESQsgAAAAfw77kI6AYzrbSJqBmMojtD7XuD6nXkKs3DQMOBHMObIA4COLhzUgr3QcZaUPFqBM9Fpr4YCK2uwOBdxZE7AdETXEB/M5N4wAACAAQAAgAEAAIBPAQQ1h88DVqwD9IAAAAH5CK5KZrD/oasUtVrwzkjypwIly5AQkC1pAa+QuT6PgQJRrxXgW7i36sGJWz9fR//v7NgyGgLvIimPidCiA33wYBBg86CzMAAAgAEAAIABAACATwEENYfPA325Ro0AAAAAgN63GqLxTu1/NyL0SV4a0Hn1n8Dzg+Wye9nbb16ZISADr+s+pcKnDcSqKHKWSl4v8Rcq80ZqG/7QObYmZUl/xUYQ7QIQyDAAAIABAACAAAAAAAABASuAlpgAAAAAACIAINCp0IUCw4KZ8J/JokbAV1TBQtK4m6WLzUomP5VBhszOAQVpUiEC2FTFYM/mwE4L60Q0G2p5QElV7YlMD7fcgoJEH79pLLEhAwJn/wsRl0hvcYj5Y3Bv3uQlxZ57pBZ9KSeuEPVNmjS/IQNvzitZiz5ksZFSQuRibjPP4pwo+OWOqZLBL2x5ZrFVqVOuIgYC2FTFYM/mwE4L60Q0G2p5QElV7YlMD7fcgoJEH79pLLEYH8zk3jAAAIABAACAAQAAgAAAAAAAAAAAIgYDAmf/CxGXSG9xiPljcG/e5CXFnnukFn0pJ64Q9U2aNL8YYPOgszAAAIABAACAAQAAgAAAAAAAAAAAIgYDb84rWYs+ZLGRUkLkYm4zz+KcKPjljqmSwS9seWaxVakY7QIQyDAAAIABAACAAAAAAAAAAAAAAAAAAAAA", Network.RegTest); result.Should().BeEquivalentTo(psbt); } @@ -358,10 +358,10 @@ async Task GenerateTemplatePSBT_SingleSigSucceeds() var result = await bitcoinService.GenerateTemplatePSBT(withdrawalRequest); // Assert - var psbt = PSBT.Parse("cHNidP8BAIkBAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/////wD/////AkBCDwAAAAAAIgAgPaPWaBQgTxHOMVfMfpX21blroUe8KAd6w2gLRelFuiCsUYkAAAAAACIAIDx3862ZOy+vKdDZ4oysyRZX0HARoqQ9LqqK2ukxoopiAAAAAE8BBDWHzwN9uUaNAAAAAYPR/OiA1LbTzxbLPvbXvtAwckIG3g+0T1zblR/ZodaiA5zBFsigPpL8htN/KJ/Ph8SPvQA/K+mSNXTSA0hgvPNuEO0CEMgwAACAAQAAgAEAAAAAAQEfgJaYAAAAAAAWABTpOvUBMqNMfl7P81etji6x4fXrMyIGA3uD9HVjgF5E+eQhHp+Na6femVYpc4bCA4DmimehAdWcGO0CEMgwAACAAQAAgAEAAAAAAAAAAAAAAAAAAA==", Network.RegTest); + var psbt = PSBT.Parse("cHNidP8BAIkBAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/////wD9////AkBCDwAAAAAAIgAgPaPWaBQgTxHOMVfMfpX21blroUe8KAd6w2gLRelFuiCsUYkAAAAAACIAIDx3862ZOy+vKdDZ4oysyRZX0HARoqQ9LqqK2ukxoopiAAAAAE8BBDWHzwN9uUaNAAAAAYPR/OiA1LbTzxbLPvbXvtAwckIG3g+0T1zblR/ZodaiA5zBFsigPpL8htN/KJ/Ph8SPvQA/K+mSNXTSA0hgvPNuEO0CEMgwAACAAQAAgAEAAAAAAQEfgJaYAAAAAAAWABTpOvUBMqNMfl7P81etji6x4fXrMyIGA3uD9HVjgF5E+eQhHp+Na6femVYpc4bCA4DmimehAdWcGO0CEMgwAACAAQAAgAEAAAAAAAAAAAAAAAAAAA==", Network.RegTest); result.Should().BeEquivalentTo(psbt); } - + [Fact] async Task GenerateTemplatePSBT_SingleSigFailsFrozenUTXO() { @@ -546,10 +546,10 @@ async Task GenerateTemplatePSBT_SingleSigSuccessManuallyUnfrozenUTXO() var result = await bitcoinService.GenerateTemplatePSBT(withdrawalRequest); // Assert - var psbt = PSBT.Parse("cHNidP8BAIkBAAAAAdIEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAD/////AkBCDwAAAAAAIgAgPaPWaBQgTxHOMVfMfpX21blroUe8KAd6w2gLRelFuiCsUYkAAAAAACIAIDx3862ZOy+vKdDZ4oysyRZX0HARoqQ9LqqK2ukxoopiAAAAAE8BBDWHzwN9uUaNAAAAAYPR/OiA1LbTzxbLPvbXvtAwckIG3g+0T1zblR/ZodaiA5zBFsigPpL8htN/KJ/Ph8SPvQA/K+mSNXTSA0hgvPNuEO0CEMgwAACAAQAAgAEAAAAAAQEfgJaYAAAAAAAWABTpOvUBMqNMfl7P81etji6x4fXrMyIGA3uD9HVjgF5E+eQhHp+Na6femVYpc4bCA4DmimehAdWcGO0CEMgwAACAAQAAgAEAAAAAAAAAAAAAAAAAAA==", Network.RegTest); + var psbt = PSBT.Parse("cHNidP8BAIkBAAAAAdIEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAD9////AkBCDwAAAAAAIgAgPaPWaBQgTxHOMVfMfpX21blroUe8KAd6w2gLRelFuiCsUYkAAAAAACIAIDx3862ZOy+vKdDZ4oysyRZX0HARoqQ9LqqK2ukxoopiAAAAAE8BBDWHzwN9uUaNAAAAAYPR/OiA1LbTzxbLPvbXvtAwckIG3g+0T1zblR/ZodaiA5zBFsigPpL8htN/KJ/Ph8SPvQA/K+mSNXTSA0hgvPNuEO0CEMgwAACAAQAAgAEAAAAAAQEfgJaYAAAAAAAWABTpOvUBMqNMfl7P81etji6x4fXrMyIGA3uD9HVjgF5E+eQhHp+Na6femVYpc4bCA4DmimehAdWcGO0CEMgwAACAAQAAgAEAAAAAAAAAAAAAAAAAAA==", Network.RegTest); result.Should().BeEquivalentTo(psbt); } - + [Fact] async Task GenerateTemplatePSBT_SingleSigFailsManuallyFrozenUTXO() { @@ -647,7 +647,7 @@ await act .ThrowAsync() .WithMessage("Exception of type 'NodeGuard.Helpers.NoUTXOsAvailableException' was thrown."); } - + [Fact] async Task GenerateTemplatePSBT_Changeless_SingleSigSucceeds() { @@ -716,7 +716,7 @@ async Task GenerateTemplatePSBT_Changeless_SingleSigSucceeds() } }); - var fmUtxos = utxos.Select(x => new FMUTXO() { TxId = x.Outpoint.Hash.ToString(), OutputIndex = 1}).ToList(); + var fmUtxos = utxos.Select(x => new FMUTXO() { TxId = x.Outpoint.Hash.ToString(), OutputIndex = 1 }).ToList(); fmutxoRepository .Setup(x => x.GetLockedUTXOs(null, null)) .ReturnsAsync(fmUtxos); @@ -733,7 +733,7 @@ async Task GenerateTemplatePSBT_Changeless_SingleSigSucceeds() var result = await bitcoinService.GenerateTemplatePSBT(withdrawalRequest); // Assert - var psbt = PSBT.Parse("cHNidP8BAF4BAAAAAdIKH+sAAAAAjKlUqwAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAD/////AZiUmAAAAAAAIgAgPaPWaBQgTxHOMVfMfpX21blroUe8KAd6w2gLRelFuiAAAAAATwEENYfPA325Ro0AAAABg9H86IDUttPPFss+9te+0DByQgbeD7RPXNuVH9mh1qIDnMEWyKA+kvyG038on8+HxI+9AD8r6ZI1dNIDSGC8824Q7QIQyDAAAIABAACAAQAAAAABAR+AlpgAAAAAABYAFOk69QEyo0x+Xs/zV62OLrHh9eszIgYDe4P0dWOAXkT55CEen41rp96ZVilzhsIDgOaKZ6EB1ZwY7QIQyDAAAIABAACAAQAAAAAAAAAAAAAAAAA=", Network.RegTest); + var psbt = PSBT.Parse("cHNidP8BAF4BAAAAAdIKH+sAAAAAjKlUqwAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAD9////AZiUmAAAAAAAIgAgPaPWaBQgTxHOMVfMfpX21blroUe8KAd6w2gLRelFuiAAAAAATwEENYfPA325Ro0AAAABg9H86IDUttPPFss+9te+0DByQgbeD7RPXNuVH9mh1qIDnMEWyKA+kvyG038on8+HxI+9AD8r6ZI1dNIDSGC8824Q7QIQyDAAAIABAACAAQAAAAABAR+AlpgAAAAAABYAFOk69QEyo0x+Xs/zV62OLrHh9eszIgYDe4P0dWOAXkT55CEen41rp96ZVilzhsIDgOaKZ6EB1ZwY7QIQyDAAAIABAACAAQAAAAAAAAAAAAAAAAA=", Network.RegTest); result.Should().BeEquivalentTo(psbt); } @@ -805,7 +805,7 @@ async Task PerformWithdrawal_SingleSigSucceeds() .ReturnsAsync(new BroadcastResult() { Success = true }); nodeRepository .Setup(x => x.GetAllManagedByNodeGuard(It.IsAny())) - .Returns(Task.FromResult(new List() {node})); + .Returns(Task.FromResult(new List() { node })); var bitcoinService = new BitcoinService(_logger, null, walletWithdrawalRequestRepository.Object, null, nodeRepository.Object, null, nbXplorerService.Object, null); // Act @@ -891,7 +891,7 @@ async Task PerformWithdrawal_MultiSigSucceeds() .ReturnsAsync(new BroadcastResult() { Success = true }); nodeRepository .Setup(x => x.GetAllManagedByNodeGuard(It.IsAny())) - .Returns(Task.FromResult(new List() {node})); + .Returns(Task.FromResult(new List() { node })); var bitcoinService = new BitcoinService(_logger, null, walletWithdrawalRequestRepository.Object, null, nodeRepository.Object, null, nbXplorerService.Object, null); // Act @@ -977,7 +977,7 @@ async Task PerformWithdrawal_LegacyMultiSigSucceeds() .ReturnsAsync(new BroadcastResult() { Success = true }); nodeRepository .Setup(x => x.GetAllManagedByNodeGuard(It.IsAny())) - .Returns(Task.FromResult(new List() {node})); + .Returns(Task.FromResult(new List() { node })); var bitcoinService = new BitcoinService(_logger, null, walletWithdrawalRequestRepository.Object, null, nodeRepository.Object, null, nbXplorerService.Object, null); // Act @@ -1072,17 +1072,17 @@ async Task GenerateTemplatePSBT_MultipleDestinations_SingleSigSucceeds() // Assert result.Should().NotBeNull(); - + // Verify that the PSBT has the expected number of outputs // 3 destination outputs + 1 change output = 4 total outputs result.Outputs.Count.Should().Be(4); - + // Verify destination amounts are present (order may vary due to shuffling) var outputValues = result.Outputs.Select(o => o.Value).ToList(); outputValues.Should().Contain(new Money(0.005m, MoneyUnit.BTC)); outputValues.Should().Contain(new Money(0.003m, MoneyUnit.BTC)); outputValues.Should().Contain(new Money(0.002m, MoneyUnit.BTC)); - + // Verify that there is a change output (should be greater than the destination amounts) var changeOutput = outputValues.Where(v => v > new Money(0.005m, MoneyUnit.BTC)).FirstOrDefault(); changeOutput.Should().NotBeNull(); @@ -1118,7 +1118,7 @@ async Task GenerateTemplatePSBT_WithdrawAllFunds_SingleSigSucceeds() var nbXplorerService = new Mock(); var utxoTagRepository = new Mock(); var mapper = new Mock(); - + walletWithdrawalRequestRepository .Setup((w) => w.GetById(It.IsAny())) .ReturnsAsync(withdrawalRequest); @@ -1211,7 +1211,7 @@ async Task GenerateTemplatePSBT_WithdrawAllFunds_MultipleDestinations_ShouldFail var nbXplorerService = new Mock(); var utxoTagRepository = new Mock(); var mapper = new Mock(); - + walletWithdrawalRequestRepository .Setup((w) => w.GetById(It.IsAny())) .ReturnsAsync(withdrawalRequest); @@ -1303,7 +1303,7 @@ async Task GenerateTemplatePSBT_Changeless_MultipleDestinations_ShouldFail() var nbXplorerService = new Mock(); var utxoTagRepository = new Mock(); var mapper = new Mock(); - + walletWithdrawalRequestRepository .Setup((w) => w.GetById(It.IsAny())) .ReturnsAsync(withdrawalRequest); @@ -1401,7 +1401,7 @@ async Task GenerateTemplatePSBT_ReuseTemplatePSBT_WhenUTXOsStillValid() var walletWithdrawalRequestRepository = new Mock(); var nbXplorerService = new Mock(); - + walletWithdrawalRequestRepository .Setup((w) => w.GetById(It.IsAny())) .ReturnsAsync(withdrawalRequest); From ce61f7c204a138988b9fd24a37168715f6030901 Mon Sep 17 00:00:00 2001 From: Marcos Date: Fri, 25 Jul 2025 12:36:17 +0200 Subject: [PATCH 37/49] fix: error on failed broadcasting --- src/Jobs/PerformWithdrawalJob.cs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/Jobs/PerformWithdrawalJob.cs b/src/Jobs/PerformWithdrawalJob.cs index 74c68839..8785c5e9 100644 --- a/src/Jobs/PerformWithdrawalJob.cs +++ b/src/Jobs/PerformWithdrawalJob.cs @@ -58,6 +58,29 @@ public async Task Execute(IJobExecutionContext context) catch (Exception e) { var request = await _walletWithdrawalRequestRepository.GetById(withdrawalRequestId); + + // If an error occurs and the wd was bumping another one, we need to update the status of the withdrawal request to previous status + if (request == null && request!.BumpingId.HasValue) + { + var bumped = await _walletWithdrawalRequestRepository.GetById(request.BumpingId.Value); + if (bumped == null) + { + _logger.LogError("Failed to find withdrawal request with ID {WithdrawalRequestId}", withdrawalRequestId); + return; + } + + bumped.Status = WalletWithdrawalRequestStatus.OnChainConfirmationPending; + var (ok, _) = _walletWithdrawalRequestRepository.Update(bumped); + if (!ok) + { + _logger.LogError("Failed to update withdrawal request status to OnChainConfirmationPending for ID {WithdrawalRequestId}", withdrawalRequestId); + } + else + { + _logger.LogInformation("Updated withdrawal request status to OnChainConfirmationPending for ID {WithdrawalRequestId}", withdrawalRequestId); + } + } + request!.Status = WalletWithdrawalRequestStatus.Failed; var (updated, _) = _walletWithdrawalRequestRepository.Update(request); if (!updated) From 31aaa4ea7bc6e494562a6e0768ac7612d2b6335d Mon Sep 17 00:00:00 2001 From: Marcos Date: Fri, 25 Jul 2025 13:13:28 +0200 Subject: [PATCH 38/49] feat: add mempool profile to docker --- docker/docker-compose.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 287a0022..da2acdfe 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -60,6 +60,7 @@ services: retries: 5 mempool-frontend-btc: + profiles: ["mempool"] environment: FRONTEND_HTTP_PORT: "8080" BACKEND_MAINNET_HTTP_HOST: "mempool-backend-btc" @@ -74,6 +75,7 @@ services: - 7084:8080 mempool-backend-btc: + profiles: ["mempool"] environment: MEMPOOL_BACKEND: "electrum" CORE_RPC_HOST: "host.docker.internal" @@ -100,6 +102,7 @@ services: - mempool-backend-btc-data:/backend/cache mempool-db-btc: + profiles: ["mempool"] environment: MYSQL_DATABASE: "mempool_btc" MYSQL_USER: "mempool" @@ -112,6 +115,7 @@ services: - mempool-db-btc-data:/var/lib/mysql electrumx: + profiles: ["mempool"] image: andgohq/electrumx:1.8.7 container_name: 40swap_electrumx command: ["wait-for-it.sh", "host.docker.internal:18443", "--", "init"] From 57facd62353750fed8326a0339f14e1dee7fa2ba Mon Sep 17 00:00:00 2001 From: Marcos Date: Fri, 25 Jul 2025 15:06:51 +0200 Subject: [PATCH 39/49] fix: old migration --- ...230309121807_UpdateSourceDestChannelNodeIdFromRequests.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Migrations/20230309121807_UpdateSourceDestChannelNodeIdFromRequests.cs b/src/Migrations/20230309121807_UpdateSourceDestChannelNodeIdFromRequests.cs index 53719ef8..2f84b515 100644 --- a/src/Migrations/20230309121807_UpdateSourceDestChannelNodeIdFromRequests.cs +++ b/src/Migrations/20230309121807_UpdateSourceDestChannelNodeIdFromRequests.cs @@ -31,7 +31,10 @@ ORDER BY ""CreationDatetime"" protected override void Down(MigrationBuilder migrationBuilder) { - string query = @"UPDATE public.""Channels"" SET ""SourceNodeId"" = 1, ""DestinationNodeId"" = 1;"; + string query = @" +BEGIN TRANSACTION; + UPDATE public.""Channels"" SET ""SourceNodeId"" = 1, ""DestinationNodeId"" = 1; +COMMIT;"; migrationBuilder.Sql(query); } } From 2734d2c9849025d4f45ad8f794f459c4f7ce35eb Mon Sep 17 00:00:00 2001 From: Marcos Date: Fri, 25 Jul 2025 15:08:12 +0200 Subject: [PATCH 40/49] refactor: change from sats vb to sat vb --- src/Pages/ChannelRequests.razor | 6 +++--- src/Pages/Withdrawals.razor | 6 +++--- src/Shared/BumpfeeModal.razor | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Pages/ChannelRequests.razor b/src/Pages/ChannelRequests.razor index 46c60a88..624b7aeb 100644 --- a/src/Pages/ChannelRequests.razor +++ b/src/Pages/ChannelRequests.razor @@ -182,7 +182,7 @@ - @(context.MempoolRecommendedFeesType == MempoolRecommendedFeesType.CustomFee ? $"{context.FeeRate} sats/vb" : _selectedMempoolRecommendedFeesType.ToString().Humanize()) + @(context.MempoolRecommendedFeesType == MempoolRecommendedFeesType.CustomFee ? $"{context.FeeRate} sat/vb" : _selectedMempoolRecommendedFeesType.ToString().Humanize())
@@ -195,7 +195,7 @@ Custom - + @@ -318,7 +318,7 @@ - @(context.FeeRate != null ? $"{context.FeeRate} sats/vbyte" : _selectedMempoolRecommendedFeesType.ToString().Humanize()) + @(context.FeeRate != null ? $"{context.FeeRate} sat/vbyte" : _selectedMempoolRecommendedFeesType.ToString().Humanize()) diff --git a/src/Pages/Withdrawals.razor b/src/Pages/Withdrawals.razor index f89744e3..b76c0c62 100644 --- a/src/Pages/Withdrawals.razor +++ b/src/Pages/Withdrawals.razor @@ -201,7 +201,7 @@ - @(context.MempoolRecommendedFeesType == MempoolRecommendedFeesType.CustomFee ? $"Custom fee ({context.CustomFeeRate} sats/vb)" : $"{context.MempoolRecommendedFeesType.Humanize()} ({context.CustomFeeRate} sats/vb)") + @(context.MempoolRecommendedFeesType == MempoolRecommendedFeesType.CustomFee ? $"Custom fee ({context.CustomFeeRate} sat/vb)" : $"{context.MempoolRecommendedFeesType.Humanize()} ({context.CustomFeeRate} sat/vb)")
@@ -214,7 +214,7 @@ Custom - + @@ -346,7 +346,7 @@ - @(context.MempoolRecommendedFeesType == MempoolRecommendedFeesType.CustomFee ? $"Custom fee ({context.CustomFeeRate} sats/vb)" : $"{context.MempoolRecommendedFeesType.Humanize()} ({context.CustomFeeRate} sats/vb)") + @(context.MempoolRecommendedFeesType == MempoolRecommendedFeesType.CustomFee ? $"Custom fee ({context.CustomFeeRate} sat/vb)" : $"{context.MempoolRecommendedFeesType.Humanize()} ({context.CustomFeeRate} sat/vb)") diff --git a/src/Shared/BumpfeeModal.razor b/src/Shared/BumpfeeModal.razor index 987a131f..08152f11 100644 --- a/src/Shared/BumpfeeModal.razor +++ b/src/Shared/BumpfeeModal.razor @@ -22,7 +22,7 @@ Custom - +
@(_selectedMempoolRecommendedFeesType != MempoolRecommendedFeesType.CustomFee ? "Fees may change by the time the request is first signed" : "") @@ -48,7 +48,7 @@ @@ -99,7 +99,7 @@ var oldFee = WithdrawalRequest?.CustomFeeRate ?? 0; if (_customSatPerVbAmount <= oldFee) { - ToastService.ShowError($"Fee must be greater than the current one ({oldFee} sats/vB)"); + ToastService.ShowError($"Fee must be greater than the current one ({oldFee} sat/vb)"); } else { From e53609042bbef2b55e95451d81bc1f4fb9bf208b Mon Sep 17 00:00:00 2001 From: Marcos Date: Fri, 25 Jul 2025 21:17:01 +0200 Subject: [PATCH 41/49] fix: casing --- src/Shared/BumpfeeModal.razor | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Shared/BumpfeeModal.razor b/src/Shared/BumpfeeModal.razor index 08152f11..86009416 100644 --- a/src/Shared/BumpfeeModal.razor +++ b/src/Shared/BumpfeeModal.razor @@ -11,7 +11,7 @@ - New Fee Rate + New fee rate
- +
@(_selectedMempoolRecommendedFeesType != MempoolRecommendedFeesType.CustomFee ? "Fees may change by the time the request is first signed" : "") @@ -54,6 +59,11 @@
@(_selectedMempoolRecommendedFeesType != MempoolRecommendedFeesType.CustomFee ? "Fees must be greater than the current one" : "") + @if (_txVSize != 0) + { + @(_txVSize * _customSatPerVbAmount) sats will be approximately used as fees in the transaction. + And that's @(100 * (_txVSize * _customSatPerVbAmount) / _utxosInputSatsAmount)% of the input UTXOs + } @@ -94,6 +104,25 @@ private Modal? _modalRef; + private long _txVSize; + private long _utxosInputSatsAmount; + + protected override async Task OnParametersSetAsync() + { + if (WithdrawalRequest != null) + { + var txInfo = await NBXplorerService.GetTransactionAsync(uint256.Parse(WithdrawalRequest.TxId)); + if (txInfo != null) + { + var tx = txInfo.Transaction; + _txVSize = tx.GetVirtualSize(); + } + + if (WithdrawalRequest.UTXOs != null) + _utxosInputSatsAmount = WithdrawalRequest.UTXOs.Sum(x => x.SatsAmount); + } + } + private void HandleOnClick() { var oldFee = WithdrawalRequest?.CustomFeeRate ?? 0; From a7892ca705b4469fd978738ac45110d05e00d8b2 Mon Sep 17 00:00:00 2001 From: Marcos Date: Tue, 29 Jul 2025 18:18:38 +0200 Subject: [PATCH 46/49] fix: refactor variable name --- src/Data/Models/WalletWithdrawalRequest.cs | 4 +- src/Jobs/PerformWithdrawalJob.cs | 4 +- ...50611142943_MigrateLegacyWithdrawalData.cs | 6 +- ...50729130729_AddBumpingRequest.Designer.cs} | 65 +++++++++++++++---- ...cs => 20250729130729_AddBumpingRequest.cs} | 18 ++--- .../ApplicationDbContextModelSnapshot.cs | 53 +++++++-------- src/Pages/Withdrawals.razor | 8 +-- src/Shared/BumpfeeModal.razor | 2 +- 8 files changed, 94 insertions(+), 66 deletions(-) rename src/Migrations/{20250702080756_BumpingRequest.Designer.cs => 20250729130729_AddBumpingRequest.Designer.cs} (95%) rename src/Migrations/{20250702080756_BumpingRequest.cs => 20250729130729_AddBumpingRequest.cs} (70%) diff --git a/src/Data/Models/WalletWithdrawalRequest.cs b/src/Data/Models/WalletWithdrawalRequest.cs index 66b1cda8..0a72993c 100644 --- a/src/Data/Models/WalletWithdrawalRequest.cs +++ b/src/Data/Models/WalletWithdrawalRequest.cs @@ -194,9 +194,9 @@ public bool Equals(WalletWithdrawalRequest? other) public Wallet Wallet { get; set; } - public int? BumpingId { get; set; } + public int? BumpingWalletWithdrawalRequestId { get; set; } - public WalletWithdrawalRequest? Bumping { get; set; } + public WalletWithdrawalRequest? BumpingWalletWithdrawalRequest { get; set; } public List WalletWithdrawalRequestPSBTs { get; set; } diff --git a/src/Jobs/PerformWithdrawalJob.cs b/src/Jobs/PerformWithdrawalJob.cs index 11876490..ba016d3c 100644 --- a/src/Jobs/PerformWithdrawalJob.cs +++ b/src/Jobs/PerformWithdrawalJob.cs @@ -60,9 +60,9 @@ public async Task Execute(IJobExecutionContext context) var request = await _walletWithdrawalRequestRepository.GetById(withdrawalRequestId); // If an error occurs and the wd was bumping another one, we need to update the status of the withdrawal request to previous status - if (request != null && request!.BumpingId.HasValue) + if (request != null && request!.BumpingWalletWithdrawalRequestId.HasValue) { - var bumped = await _walletWithdrawalRequestRepository.GetById(request.BumpingId.Value); + var bumped = await _walletWithdrawalRequestRepository.GetById(request.BumpingWalletWithdrawalRequestId.Value); if (bumped == null) { _logger.LogError("Failed to find withdrawal request with ID {WithdrawalRequestId}", withdrawalRequestId); diff --git a/src/Migrations/20250611142943_MigrateLegacyWithdrawalData.cs b/src/Migrations/20250611142943_MigrateLegacyWithdrawalData.cs index 12615403..d23851f7 100644 --- a/src/Migrations/20250611142943_MigrateLegacyWithdrawalData.cs +++ b/src/Migrations/20250611142943_MigrateLegacyWithdrawalData.cs @@ -22,10 +22,8 @@ protected override void Up(MigrationBuilder migrationBuilder) ""UpdateDatetime"" FROM ""WalletWithdrawalRequests"" WHERE ""Amount"" > 0 AND ""DestinationAddress"" IS NOT NULL AND ""DestinationAddress"" != ''; - "); - - // Step 2: Verify migration was successful - migrationBuilder.Sql(@" + + -- Step 2: Verify migration was successful - if any records failed to migrate, rollback DO $$ DECLARE original_count INTEGER; diff --git a/src/Migrations/20250702080756_BumpingRequest.Designer.cs b/src/Migrations/20250729130729_AddBumpingRequest.Designer.cs similarity index 95% rename from src/Migrations/20250702080756_BumpingRequest.Designer.cs rename to src/Migrations/20250729130729_AddBumpingRequest.Designer.cs index ffdd77b5..f0262e6a 100644 --- a/src/Migrations/20250702080756_BumpingRequest.Designer.cs +++ b/src/Migrations/20250729130729_AddBumpingRequest.Designer.cs @@ -14,8 +14,8 @@ namespace NodeGuard.Migrations { [DbContext(typeof(ApplicationDbContext))] - [Migration("20250702080756_BumpingRequest")] - partial class BumpingRequest + [Migration("20250729130729_AddBumpingRequest")] + partial class AddBumpingRequest { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -850,10 +850,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - b.Property("Amount") - .HasColumnType("numeric"); - - b.Property("BumpingId") + b.Property("BumpingWalletWithdrawalRequestId") .HasColumnType("integer"); b.Property("Changeless") @@ -869,10 +866,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("text"); - b.Property("DestinationAddress") - .IsRequired() - .HasColumnType("text"); - b.Property("MempoolRecommendedFeesType") .HasColumnType("integer"); @@ -905,7 +898,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.HasKey("Id"); - b.HasIndex("BumpingId"); + b.HasIndex("BumpingWalletWithdrawalRequestId"); b.HasIndex("UserRequestorId"); @@ -914,6 +907,37 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) 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") @@ -1224,9 +1248,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) modelBuilder.Entity("NodeGuard.Data.Models.WalletWithdrawalRequest", b => { - b.HasOne("NodeGuard.Data.Models.WalletWithdrawalRequest", "Bumping") + b.HasOne("NodeGuard.Data.Models.WalletWithdrawalRequest", "BumpingWalletWithdrawalRequest") .WithMany() - .HasForeignKey("BumpingId"); + .HasForeignKey("BumpingWalletWithdrawalRequestId"); b.HasOne("NodeGuard.Data.Models.ApplicationUser", "UserRequestor") .WithMany("WalletWithdrawalRequests") @@ -1238,13 +1262,24 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.Navigation("Bumping"); + b.Navigation("BumpingWalletWithdrawalRequest"); 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") @@ -1292,6 +1327,8 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) modelBuilder.Entity("NodeGuard.Data.Models.WalletWithdrawalRequest", b => { + b.Navigation("WalletWithdrawalRequestDestinations"); + b.Navigation("WalletWithdrawalRequestPSBTs"); }); diff --git a/src/Migrations/20250702080756_BumpingRequest.cs b/src/Migrations/20250729130729_AddBumpingRequest.cs similarity index 70% rename from src/Migrations/20250702080756_BumpingRequest.cs rename to src/Migrations/20250729130729_AddBumpingRequest.cs index b8453a9e..de78161e 100644 --- a/src/Migrations/20250702080756_BumpingRequest.cs +++ b/src/Migrations/20250729130729_AddBumpingRequest.cs @@ -5,26 +5,26 @@ namespace NodeGuard.Migrations { /// - public partial class BumpingRequest : Migration + public partial class AddBumpingRequest : Migration { /// protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.AddColumn( - name: "BumpingId", + name: "BumpingWalletWithdrawalRequestId", table: "WalletWithdrawalRequests", type: "integer", nullable: true); migrationBuilder.CreateIndex( - name: "IX_WalletWithdrawalRequests_BumpingId", + name: "IX_WalletWithdrawalRequests_BumpingWalletWithdrawalRequestId", table: "WalletWithdrawalRequests", - column: "BumpingId"); + column: "BumpingWalletWithdrawalRequestId"); migrationBuilder.AddForeignKey( - name: "FK_WalletWithdrawalRequests_WalletWithdrawalRequests_BumpingId", + name: "FK_WalletWithdrawalRequests_WalletWithdrawalRequests_BumpingWa~", table: "WalletWithdrawalRequests", - column: "BumpingId", + column: "BumpingWalletWithdrawalRequestId", principalTable: "WalletWithdrawalRequests", principalColumn: "Id"); } @@ -33,15 +33,15 @@ protected override void Up(MigrationBuilder migrationBuilder) protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropForeignKey( - name: "FK_WalletWithdrawalRequests_WalletWithdrawalRequests_BumpingId", + name: "FK_WalletWithdrawalRequests_WalletWithdrawalRequests_BumpingWa~", table: "WalletWithdrawalRequests"); migrationBuilder.DropIndex( - name: "IX_WalletWithdrawalRequests_BumpingId", + name: "IX_WalletWithdrawalRequests_BumpingWalletWithdrawalRequestId", table: "WalletWithdrawalRequests"); migrationBuilder.DropColumn( - name: "BumpingId", + name: "BumpingWalletWithdrawalRequestId", table: "WalletWithdrawalRequests"); } } diff --git a/src/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Migrations/ApplicationDbContextModelSnapshot.cs index e0e4e1f6..91e8fa82 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", (string)null); + b.ToTable("ApplicationUserNode"); }); modelBuilder.Entity("ChannelOperationRequestFMUTXO", b => @@ -51,7 +51,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("UtxosId"); - b.ToTable("ChannelOperationRequestFMUTXO", (string)null); + b.ToTable("ChannelOperationRequestFMUTXO"); }); modelBuilder.Entity("FMUTXOWalletWithdrawalRequest", b => @@ -66,7 +66,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("WalletWithdrawalRequestsId"); - b.ToTable("FMUTXOWalletWithdrawalRequest", (string)null); + b.ToTable("FMUTXOWalletWithdrawalRequest"); }); modelBuilder.Entity("KeyWallet", b => @@ -81,7 +81,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("WalletsId"); - b.ToTable("KeyWallet", (string)null); + b.ToTable("KeyWallet"); }); modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => @@ -329,7 +329,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("CreatorId"); - b.ToTable("ApiTokens", (string)null); + b.ToTable("ApiTokens"); }); modelBuilder.Entity("NodeGuard.Data.Models.Channel", b => @@ -386,7 +386,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("SourceNodeId"); - b.ToTable("Channels", (string)null); + b.ToTable("Channels"); }); modelBuilder.Entity("NodeGuard.Data.Models.ChannelOperationRequest", b => @@ -466,7 +466,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("WalletId"); - b.ToTable("ChannelOperationRequests", (string)null); + b.ToTable("ChannelOperationRequests"); }); modelBuilder.Entity("NodeGuard.Data.Models.ChannelOperationRequestPSBT", b => @@ -508,7 +508,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("UserSignerId"); - b.ToTable("ChannelOperationRequestPSBTs", (string)null); + b.ToTable("ChannelOperationRequestPSBTs"); }); modelBuilder.Entity("NodeGuard.Data.Models.FMUTXO", b => @@ -537,7 +537,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); - b.ToTable("FMUTXOs", (string)null); + b.ToTable("FMUTXOs"); }); modelBuilder.Entity("NodeGuard.Data.Models.InternalWallet", b => @@ -569,7 +569,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); - b.ToTable("InternalWallets", (string)null); + b.ToTable("InternalWallets"); }); modelBuilder.Entity("NodeGuard.Data.Models.Key", b => @@ -624,7 +624,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("UserId"); - b.ToTable("Keys", (string)null); + b.ToTable("Keys"); }); modelBuilder.Entity("NodeGuard.Data.Models.LiquidityRule", b => @@ -679,7 +679,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("SwapWalletId"); - b.ToTable("LiquidityRules", (string)null); + b.ToTable("LiquidityRules"); }); modelBuilder.Entity("NodeGuard.Data.Models.Node", b => @@ -729,7 +729,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("ReturningFundsWalletId"); - b.ToTable("Nodes", (string)null); + b.ToTable("Nodes"); }); 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", (string)null); + b.ToTable("UTXOTags"); }); 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", (string)null); + b.ToTable("Wallets"); }); modelBuilder.Entity("NodeGuard.Data.Models.WalletWithdrawalRequest", b => @@ -847,10 +847,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - b.Property("Amount") - .HasColumnType("numeric"); - - b.Property("BumpingId") + b.Property("BumpingWalletWithdrawalRequestId") .HasColumnType("integer"); b.Property("Changeless") @@ -866,10 +863,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("text"); - b.Property("DestinationAddress") - .IsRequired() - .HasColumnType("text"); - b.Property("MempoolRecommendedFeesType") .HasColumnType("integer"); @@ -902,13 +895,13 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); - b.HasIndex("BumpingId"); + b.HasIndex("BumpingWalletWithdrawalRequestId"); b.HasIndex("UserRequestorId"); b.HasIndex("WalletId"); - b.ToTable("WalletWithdrawalRequests", (string)null); + b.ToTable("WalletWithdrawalRequests"); }); modelBuilder.Entity("NodeGuard.Data.Models.WalletWithdrawalRequestDestination", b => @@ -939,7 +932,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("WalletWithdrawalRequestId"); - b.ToTable("WalletWithdrawalRequestDestinations", (string)null); + b.ToTable("WalletWithdrawalRequestDestinations"); }); modelBuilder.Entity("NodeGuard.Data.Models.WalletWithdrawalRequestPSBT", b => @@ -981,7 +974,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("WalletWithdrawalRequestId"); - b.ToTable("WalletWithdrawalRequestPSBTs", (string)null); + b.ToTable("WalletWithdrawalRequestPSBTs"); }); modelBuilder.Entity("NodeGuard.Data.Models.ApplicationUser", b => @@ -1252,9 +1245,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("NodeGuard.Data.Models.WalletWithdrawalRequest", b => { - b.HasOne("NodeGuard.Data.Models.WalletWithdrawalRequest", "Bumping") + b.HasOne("NodeGuard.Data.Models.WalletWithdrawalRequest", "BumpingWalletWithdrawalRequest") .WithMany() - .HasForeignKey("BumpingId"); + .HasForeignKey("BumpingWalletWithdrawalRequestId"); b.HasOne("NodeGuard.Data.Models.ApplicationUser", "UserRequestor") .WithMany("WalletWithdrawalRequests") @@ -1266,7 +1259,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.Navigation("Bumping"); + b.Navigation("BumpingWalletWithdrawalRequest"); b.Navigation("UserRequestor"); diff --git a/src/Pages/Withdrawals.razor b/src/Pages/Withdrawals.razor index 485aaf49..110cdac0 100644 --- a/src/Pages/Withdrawals.razor +++ b/src/Pages/Withdrawals.razor @@ -1145,9 +1145,9 @@ throw new ShowToUserException(createWithdrawalResult.Item2); } - if (_selectedRequest.BumpingId != null) + if (_selectedRequest.BumpingWalletWithdrawalRequestId != null) { - WalletWithdrawalRequest originalRequest = await WalletWithdrawalRequestRepository.GetById((int)_selectedRequest.BumpingId); + WalletWithdrawalRequest originalRequest = await WalletWithdrawalRequestRepository.GetById((int)_selectedRequest.BumpingWalletWithdrawalRequestId); if (originalRequest == null) { throw new ShowToUserException("Original withdrawal request not found"); @@ -1260,9 +1260,9 @@ private async Task ResetStatusBumpedIfError(WalletWithdrawalRequest request) { - if (request.BumpingId != null) + if (request.BumpingWalletWithdrawalRequestId != null) { - var bumpedRequest = await WalletWithdrawalRequestRepository.GetById(request.BumpingId.Value); + var bumpedRequest = await WalletWithdrawalRequestRepository.GetById(request.BumpingWalletWithdrawalRequestId.Value); if (bumpedRequest != null) { diff --git a/src/Shared/BumpfeeModal.razor b/src/Shared/BumpfeeModal.razor index e18c7f96..127e31cb 100644 --- a/src/Shared/BumpfeeModal.razor +++ b/src/Shared/BumpfeeModal.razor @@ -139,7 +139,7 @@ newRequest.MempoolRecommendedFeesType = _selectedMempoolRecommendedFeesType; newRequest.CustomFeeRate = _customSatPerVbAmount; newRequest.UserRequestorId = WithdrawalRequest.UserRequestorId; - newRequest.BumpingId = WithdrawalRequest.Id; + newRequest.BumpingWalletWithdrawalRequestId = WithdrawalRequest.Id; SubmitBumpfeeModal?.Invoke(newRequest); } } From a766a865b065bd42168fb697c81c8038c2b7a019 Mon Sep 17 00:00:00 2001 From: Marcos Date: Wed, 30 Jul 2025 11:26:10 +0200 Subject: [PATCH 47/49] fix: spacing --- src/Pages/Withdrawals.razor | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Pages/Withdrawals.razor b/src/Pages/Withdrawals.razor index 110cdac0..9ea26ecf 100644 --- a/src/Pages/Withdrawals.razor +++ b/src/Pages/Withdrawals.razor @@ -1308,8 +1308,10 @@ private async Task ShowActionDropdown(WalletWithdrawalRequest walletWithdrawalRequest) { ulong confirmations = 0; - if (!string.IsNullOrEmpty(walletWithdrawalRequest.TxId)) { - try { + if (!string.IsNullOrEmpty(walletWithdrawalRequest.TxId)) + { + try + { var nbxplorerStatus = await NBXplorerService.GetTransactionAsync(uint256.Parse(walletWithdrawalRequest.TxId)); confirmations = (ulong)(nbxplorerStatus?.Confirmations ?? throw new Exception("Failed to get confirmations")); return walletWithdrawalRequest.Status == WalletWithdrawalRequestStatus.OnChainConfirmationPending && confirmations.Equals((ulong) 0); From 245bfbc00b47da699561aeecc59775375b15b785 Mon Sep 17 00:00:00 2001 From: Marcos Date: Wed, 30 Jul 2025 14:34:16 +0200 Subject: [PATCH 48/49] feat: add max of 200 --- src/Shared/BumpfeeModal.razor | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Shared/BumpfeeModal.razor b/src/Shared/BumpfeeModal.razor index 127e31cb..0e6dbcb5 100644 --- a/src/Shared/BumpfeeModal.razor +++ b/src/Shared/BumpfeeModal.razor @@ -27,6 +27,7 @@ CurrencySymbolPlacement="CurrencySymbolPlacement.Suffix" CurrencySymbol=" sat/vb" Min="1" + Max="200" Disabled="@(_selectedMempoolRecommendedFeesType != MempoolRecommendedFeesType.CustomFee)">
@@ -111,11 +112,14 @@ { if (WithdrawalRequest != null) { - var txInfo = await NBXplorerService.GetTransactionAsync(uint256.Parse(WithdrawalRequest.TxId)); - if (txInfo != null) + if (WithdrawalRequest.TxId != null) { - var tx = txInfo.Transaction; - _txVSize = tx.GetVirtualSize(); + var txInfo = await NBXplorerService.GetTransactionAsync(uint256.Parse(WithdrawalRequest.TxId)); + if (txInfo != null) + { + var tx = txInfo.Transaction; + _txVSize = tx.GetVirtualSize(); + } } if (WithdrawalRequest.UTXOs != null) From 0032dbad1d6e56daf0603c2559d40fc8e6e3c0d0 Mon Sep 17 00:00:00 2001 From: Marcos Date: Thu, 31 Jul 2025 16:35:27 +0200 Subject: [PATCH 49/49] fix: bug on multisig --- src/Pages/Withdrawals.razor | 108 ++++++++++++++++++++---------------- 1 file changed, 60 insertions(+), 48 deletions(-) diff --git a/src/Pages/Withdrawals.razor b/src/Pages/Withdrawals.razor index 9ea26ecf..d9c4ca0d 100644 --- a/src/Pages/Withdrawals.razor +++ b/src/Pages/Withdrawals.razor @@ -780,6 +780,11 @@ //PSBT Generation try { + if (_selectedRequest.BumpingWalletWithdrawalRequestId != null) + { + await CopyBumpedRequestData(_selectedRequest); + } + var templatePSBT = await BitcoinService.GenerateTemplatePSBT(walletWithdrawalRequest); //TODO Save template PSBT (?) @@ -1147,55 +1152,9 @@ if (_selectedRequest.BumpingWalletWithdrawalRequestId != null) { - WalletWithdrawalRequest originalRequest = await WalletWithdrawalRequestRepository.GetById((int)_selectedRequest.BumpingWalletWithdrawalRequestId); - if (originalRequest == null) - { - throw new ShowToUserException("Original withdrawal request not found"); - } - - // Get destinations from the original request - List originalDestinations = await WalletWithdrawalRequestDestinationRepository.GetByWalletWithdrawalRequestId(originalRequest.Id); - if (originalDestinations == null || originalDestinations.Count == 0) - { - throw new ShowToUserException("Original withdrawal request destinations not found"); - } - - _selectedRequest.WalletWithdrawalRequestDestinations = originalDestinations.Select(d => new WalletWithdrawalRequestDestination - { - Address = d.Address, - Amount = d.Amount, - WalletWithdrawalRequestId = _selectedRequest.Id - }).ToList(); - - // Get utxos from the original request - List mUTXOs = await FMUTXORepository.GetLockedUTXOsByWithdrawalId(originalRequest.Id); - - _selectedRequest.UTXOs = mUTXOs; - List outpoints = new List(); - mUTXOs.ForEach(u => { - OutPoint o; - if (OutPoint.TryParse($"{u.TxId}:{u.OutputIndex}", out o)) - outpoints.Add(o); - else - throw new FormatException("Failed to parse OutPoint from TxId and OutputIndex"); - }); - _selectedUTXOs = await CoinSelectionService.GetUTXOsByOutpointAsync(originalRequest.Wallet.GetDerivationStrategy(), outpoints); - originalRequest.Status = WalletWithdrawalRequestStatus.Bumped; - - var updateResult = WalletWithdrawalRequestRepository.Update(_selectedRequest); - if (!updateResult.Item1) - { - ToastService.ShowError("Something went wrong"); - return; - } - - updateResult = WalletWithdrawalRequestRepository.Update(originalRequest); - if (!updateResult.Item1) - { - ToastService.ShowError("Something went wrong"); - return; - } + await CopyBumpedRequestData(_selectedRequest); } + _selectedRequest.Changeless = _selectedUTXOs.Count > 0; ToastService.ShowSuccess("Withdrawal request created!"); @@ -1271,6 +1230,7 @@ if (!updateResult.Item1) { ToastService.ShowError("Error while updating the bumped request, please contact a superadmin for troubleshooting"); + return; } } } @@ -1319,6 +1279,7 @@ catch (Exception) { ToastService.ShowError("Error while showing bumpfee action"); + return false; } } @@ -1372,4 +1333,55 @@ return string.Empty; } + private async Task CopyBumpedRequestData(WalletWithdrawalRequest request) + { + WalletWithdrawalRequest originalRequest = await WalletWithdrawalRequestRepository.GetById((int)_selectedRequest.BumpingWalletWithdrawalRequestId); + if (originalRequest == null) + { + throw new ShowToUserException("Original withdrawal request not found"); + } + + // Get destinations from the original request + List originalDestinations = await WalletWithdrawalRequestDestinationRepository.GetByWalletWithdrawalRequestId(originalRequest.Id); + if (originalDestinations == null || originalDestinations.Count == 0) + { + throw new ShowToUserException("Original withdrawal request destinations not found"); + } + + _selectedRequest.WalletWithdrawalRequestDestinations = originalDestinations.Select(d => new WalletWithdrawalRequestDestination + { + Address = d.Address, + Amount = d.Amount, + WalletWithdrawalRequestId = _selectedRequest.Id + }).ToList(); + + // Get utxos from the original request + List mUTXOs = await FMUTXORepository.GetLockedUTXOsByWithdrawalId(originalRequest.Id); + + _selectedRequest.UTXOs = mUTXOs; + List outpoints = new List(); + mUTXOs.ForEach(u => { + OutPoint o; + if (OutPoint.TryParse($"{u.TxId}:{u.OutputIndex}", out o)) + outpoints.Add(o); + else + throw new FormatException("Failed to parse OutPoint from TxId and OutputIndex"); + }); + _selectedUTXOs = await CoinSelectionService.GetUTXOsByOutpointAsync(originalRequest.Wallet.GetDerivationStrategy(), outpoints); + originalRequest.Status = WalletWithdrawalRequestStatus.Bumped; + + var updateResult = WalletWithdrawalRequestRepository.Update(_selectedRequest); + if (!updateResult.Item1) + { + ToastService.ShowError("Something went wrong"); + return; + } + + updateResult = WalletWithdrawalRequestRepository.Update(originalRequest); + if (!updateResult.Item1) + { + ToastService.ShowError("Something went wrong"); + return; + } + } }