From d0e01768ab2ef3a1bd37d7be6b4a8bb61be6d391 Mon Sep 17 00:00:00 2001 From: Umar Bolatov Date: Thu, 9 Jun 2022 20:58:51 -0700 Subject: [PATCH] Allow excluding unconfirmed UTXOs when creating a new transaction with Greenfield API See original request: https://github.com/btcpayserver/btcpayserver/discussions/3737 --- .../Models/CreateOnChainTransactionRequest.cs | 1 + BTCPayServer.Tests/GreenfieldAPITests.cs | 12 ++++++++++++ .../GreenfieldStoreOnChainWalletsController.cs | 12 +++++++++++- BTCPayServer/Controllers/UIWalletsController.cs | 2 +- BTCPayServer/Services/Wallets/BTCPayWallet.cs | 8 ++++++-- .../v1/swagger.template.stores-wallet.on-chain.json | 6 ++++++ 6 files changed, 37 insertions(+), 4 deletions(-) diff --git a/BTCPayServer.Client/Models/CreateOnChainTransactionRequest.cs b/BTCPayServer.Client/Models/CreateOnChainTransactionRequest.cs index 7a74f3f28..4de09ff2e 100644 --- a/BTCPayServer.Client/Models/CreateOnChainTransactionRequest.cs +++ b/BTCPayServer.Client/Models/CreateOnChainTransactionRequest.cs @@ -26,5 +26,6 @@ namespace BTCPayServer.Client.Models public List Destinations { get; set; } [JsonProperty("rbf")] public bool? RBF { get; set; } = null; + public bool ExcludeUnconfirmed { get; set; } = false; } } diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs index ead5b74e2..0e34d5885 100644 --- a/BTCPayServer.Tests/GreenfieldAPITests.cs +++ b/BTCPayServer.Tests/GreenfieldAPITests.cs @@ -2078,6 +2078,18 @@ namespace BTCPayServer.Tests Assert.True((await tester.ExplorerNode.TestMempoolAcceptAsync(tx)).IsAllowed); createTxRequest.NoChange = false; + + // Validation for excluding unconfirmed UTXOs and manually selecting inputs at the same time + await AssertValidationError(new[] { "ExcludeUnconfirmed" }, async () => + { + createTxRequest.SelectedInputs = new List(); + createTxRequest.ExcludeUnconfirmed = true; + tx = await client.CreateOnChainTransactionButDoNotBroadcast(walletId.StoreId, walletId.CryptoCode, + createTxRequest, tester.ExplorerClient.Network.NBitcoinNetwork); + }); + createTxRequest.SelectedInputs = null; + createTxRequest.ExcludeUnconfirmed = false; + //coin selection await AssertValidationError(new[] { nameof(createTxRequest.SelectedInputs) }, async () => { diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldStoreOnChainWalletsController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldStoreOnChainWalletsController.cs index b1689540a..33365fedd 100644 --- a/BTCPayServer/Controllers/GreenField/GreenfieldStoreOnChainWalletsController.cs +++ b/BTCPayServer/Controllers/GreenField/GreenfieldStoreOnChainWalletsController.cs @@ -376,10 +376,20 @@ namespace BTCPayServer.Controllers.Greenfield return this.CreateValidationError(ModelState); } + if (request.SelectedInputs != null && request.ExcludeUnconfirmed == true) + { + ModelState.AddModelError( + nameof(request.ExcludeUnconfirmed), + "Can't automatically exclude unconfirmed UTXOs while selection custom inputs" + ); + + return this.CreateValidationError(ModelState); + } + var explorerClient = _explorerClientProvider.GetExplorerClient(cryptoCode); var wallet = _btcPayWalletProvider.GetWallet(network); - var utxos = await wallet.GetUnspentCoins(derivationScheme.AccountDerivation); + var utxos = await wallet.GetUnspentCoins(derivationScheme.AccountDerivation, request.ExcludeUnconfirmed); if (request.SelectedInputs != null || !utxos.Any()) { utxos = utxos.Where(coin => request.SelectedInputs?.Contains(coin.OutPoint) ?? true) diff --git a/BTCPayServer/Controllers/UIWalletsController.cs b/BTCPayServer/Controllers/UIWalletsController.cs index 433ac562e..488f6b2d0 100644 --- a/BTCPayServer/Controllers/UIWalletsController.cs +++ b/BTCPayServer/Controllers/UIWalletsController.cs @@ -615,7 +615,7 @@ namespace BTCPayServer.Controllers var walletTransactionsInfoAsync = await WalletRepository.GetWalletTransactionsInfo(walletId); var utxos = await _walletProvider.GetWallet(network) - .GetUnspentCoins(schemeSettings.AccountDerivation, cancellation); + .GetUnspentCoins(schemeSettings.AccountDerivation, false, cancellation); vm.InputsAvailable = utxos.Select(coin => { walletTransactionsInfoAsync.TryGetValue(coin.OutPoint.Hash.ToString(), out var info); diff --git a/BTCPayServer/Services/Wallets/BTCPayWallet.cs b/BTCPayServer/Services/Wallets/BTCPayWallet.cs index 96dbcf351..be8cbfc60 100644 --- a/BTCPayServer/Services/Wallets/BTCPayWallet.cs +++ b/BTCPayServer/Services/Wallets/BTCPayWallet.cs @@ -245,11 +245,15 @@ namespace BTCPayServer.Services.Wallets - public async Task GetUnspentCoins(DerivationStrategyBase derivationStrategy, CancellationToken cancellation = default(CancellationToken)) + public async Task GetUnspentCoins( + DerivationStrategyBase derivationStrategy, + bool excludeUnconfirmed = false, + CancellationToken cancellation = default(CancellationToken) + ) { ArgumentNullException.ThrowIfNull(derivationStrategy); return (await GetUTXOChanges(derivationStrategy, cancellation)) - .GetUnspentUTXOs() + .GetUnspentUTXOs(excludeUnconfirmed) .Select(c => new ReceivedCoin() { KeyPath = c.KeyPath, diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.stores-wallet.on-chain.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.stores-wallet.on-chain.json index e74684b8c..7290f35fa 100644 --- a/BTCPayServer/wwwroot/swagger/v1/swagger.template.stores-wallet.on-chain.json +++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.stores-wallet.on-chain.json @@ -870,6 +870,12 @@ "nullable": true, "description": "Whether to enable RBF for the transaction. Leave blank to have it random (beneficial to privacy)" }, + "excludeUnconfirmed": { + "type": "boolean", + "default": false, + "nullable": true, + "description": "Whether to exclude unconfirmed UTXOs from the transaction." + }, "selectedInputs": { "nullable": true, "description": "Restrict the creation of the transactions from the outpoints provided ONLY (coin selection)",