diff --git a/BTCPayServer.Client/BTCPayServerClient.OnChainWallet.cs b/BTCPayServer.Client/BTCPayServerClient.OnChainWallet.cs index 34ec9d625..c6caaaa9f 100644 --- a/BTCPayServer.Client/BTCPayServerClient.OnChainWallet.cs +++ b/BTCPayServer.Client/BTCPayServerClient.OnChainWallet.cs @@ -19,6 +19,19 @@ namespace BTCPayServer.Client CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/Onchain/{cryptoCode}/wallet"), token); return await HandleResponse(response); } + public virtual async Task GetOnChainFeeRate(string storeId, string cryptoCode, int? blockTarget = null, + CancellationToken token = default) + { + Dictionary queryParams = new Dictionary(); + if (blockTarget != null) + { + queryParams.Add("blockTarget",blockTarget); + } + var response = + await _httpClient.SendAsync( + CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/Onchain/{cryptoCode}/wallet/feeRate", queryParams), token); + return await HandleResponse(response); + } public virtual async Task GetOnChainWalletReceiveAddress(string storeId, string cryptoCode, bool forceGenerate = false, CancellationToken token = default) diff --git a/BTCPayServer.Client/Models/OnChainWalletFeeRateData.cs b/BTCPayServer.Client/Models/OnChainWalletFeeRateData.cs new file mode 100644 index 000000000..3d2e253ff --- /dev/null +++ b/BTCPayServer.Client/Models/OnChainWalletFeeRateData.cs @@ -0,0 +1,12 @@ +using NBitcoin; +using NBitcoin.JsonConverters; +using Newtonsoft.Json; + +namespace BTCPayServer.Client.Models +{ + public class OnChainWalletFeeRateData + { + [JsonConverter(typeof(FeeRateJsonConverter))] + public FeeRate FeeRate { get; set; } + } +} diff --git a/BTCPayServer.Client/Models/OnChainWalletOverviewData.cs b/BTCPayServer.Client/Models/OnChainWalletOverviewData.cs index 6f2fa51b8..b5c078c06 100644 --- a/BTCPayServer.Client/Models/OnChainWalletOverviewData.cs +++ b/BTCPayServer.Client/Models/OnChainWalletOverviewData.cs @@ -5,7 +5,6 @@ namespace BTCPayServer.Client.Models { public class OnChainWalletOverviewData { - [JsonConverter(typeof(NumericStringJsonConverter))] public decimal Balance { get; set; } } diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs index f38559f89..195e80d21 100644 --- a/BTCPayServer.Tests/GreenfieldAPITests.cs +++ b/BTCPayServer.Tests/GreenfieldAPITests.cs @@ -1435,6 +1435,10 @@ namespace BTCPayServer.Tests var overview = await client.ShowOnChainWalletOverview(walletId.StoreId, walletId.CryptoCode ); Assert.Equal(0m, overview.Balance); + + var fee = await client.GetOnChainFeeRate(walletId.StoreId, walletId.CryptoCode ); + Assert.NotNull( fee.FeeRate); + await AssertHttpError(403, async () => { await viewOnlyClient.GetOnChainWalletReceiveAddress(walletId.StoreId, walletId.CryptoCode ); diff --git a/BTCPayServer/Controllers/GreenField/StoreOnChainWalletsController.cs b/BTCPayServer/Controllers/GreenField/StoreOnChainWalletsController.cs index f793e1c70..b37f3c012 100644 --- a/BTCPayServer/Controllers/GreenField/StoreOnChainWalletsController.cs +++ b/BTCPayServer/Controllers/GreenField/StoreOnChainWalletsController.cs @@ -44,6 +44,7 @@ namespace BTCPayServer.Controllers.GreenField private readonly DelayedTransactionBroadcaster _delayedTransactionBroadcaster; private readonly EventAggregator _eventAggregator; private readonly WalletReceiveService _walletReceiveService; + private readonly IFeeProviderFactory _feeProviderFactory; public StoreOnChainWalletsController( IAuthorizationService authorizationService, @@ -57,7 +58,8 @@ namespace BTCPayServer.Controllers.GreenField PayjoinClient payjoinClient, DelayedTransactionBroadcaster delayedTransactionBroadcaster, EventAggregator eventAggregator, - WalletReceiveService walletReceiveService) + WalletReceiveService walletReceiveService, + IFeeProviderFactory feeProviderFactory) { _authorizationService = authorizationService; _btcPayWalletProvider = btcPayWalletProvider; @@ -71,6 +73,7 @@ namespace BTCPayServer.Controllers.GreenField _delayedTransactionBroadcaster = delayedTransactionBroadcaster; _eventAggregator = eventAggregator; _walletReceiveService = walletReceiveService; + _feeProviderFactory = feeProviderFactory; } [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] @@ -87,6 +90,21 @@ namespace BTCPayServer.Controllers.GreenField }); } + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] + [HttpGet("~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/feerate")] + public async Task GetOnChainFeeRate(string storeId, string cryptoCode, int? blockTarget = null) + { + if (IsInvalidWalletRequest(cryptoCode, out BTCPayNetwork network, + out DerivationSchemeSettings derivationScheme, out IActionResult actionResult)) return actionResult; + + var feeRateTarget = blockTarget?? Store.GetStoreBlob().RecommendedFeeBlockTarget; + return Ok(new OnChainWalletFeeRateData() + { + FeeRate = await _feeProviderFactory.CreateFeeProvider(network) + .GetFeeRateAsync(feeRateTarget), + }); + } + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] [HttpGet("~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/address")] public async Task GetOnChainWalletReceiveAddress(string storeId, string cryptoCode, bool forceGenerate = false) @@ -332,7 +350,7 @@ namespace BTCPayServer.Controllers.GreenField "You are sending your entire balance, you should subtract the fees from a destination", this); } - var minRelayFee = this._nbXplorerDashboard.Get(network.CryptoCode).Status.BitcoinStatus?.MinRelayTxFee ?? + var minRelayFee = _nbXplorerDashboard.Get(network.CryptoCode).Status.BitcoinStatus?.MinRelayTxFee ?? new FeeRate(1.0m); if (request.FeeRate != null && request.FeeRate < minRelayFee) { 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 9a1c5c98f..21755cebf 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 @@ -56,6 +56,72 @@ ] } }, + "/api/v1/stores/{storeId}/payment-methods/OnChain/{cryptoCode}/wallet/feeRate": { + "get": { + "tags": [ + "Store Wallet (On Chain)" + ], + "summary": "Get store on-chain wallet overview", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "The store to fetch", + "schema": { + "type": "string" + } + }, + { + "name": "cryptoCode", + "in": "path", + "required": true, + "description": "The crypto code of the payment method to fetch", + "schema": { + "type": "string" + } + }, + { + "name": "blockTarget", + "in": "query", + "required": false, + "description": "The number of blocks away you are willing to target for confirmation. Defaults to the wallet's configured `RecommendedFeeBlockTarget`", + "schema": { + "type": "number", + "minimum": 1 + } + } + ], + "description": "Get wallet onchain fee rate", + "operationId": "StoreOnChainWallets_GetOnChainFeeRate", + "responses": { + "200": { + "description": "fee rate", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OnChainWalletFeeRateData" + } + } + } + }, + "403": { + "description": "If you are authenticated but forbidden to view the specified store" + }, + "404": { + "description": "The key is not found for this store/wallet" + } + }, + "security": [ + { + "API Key": [ + "btcpay.store.canmodifystoresettings" + ], + "Basic": [] + } + ] + } + }, "/api/v1/stores/{storeId}/payment-methods/OnChain/{cryptoCode}/wallet/address": { "get": { "tags": [ @@ -449,6 +515,17 @@ } } }, + "OnChainWalletFeeRateData": { + "type": "object", + "additionalProperties": false, + "properties": { + "feeRate": { + "type": "number", + "format": "decimal", + "description": "The fee rate (sats per byte) based on the wallet's configured recommended block confirmation target" + } + } + }, "OnChainWalletAddressData": { "type": "object", "additionalProperties": false,