From 9ba4b030ed88b28da8b228e9c85bc2a3e582c218 Mon Sep 17 00:00:00 2001 From: Nicolas Dorier Date: Fri, 27 Sep 2024 15:27:04 +0900 Subject: [PATCH] Fix: Do not expose xpub without modify store permission (#6212) --- .../BTCPayServerClient.Invoices.cs | 8 +- BTCPayServer.Tests/GreenfieldAPITests.cs | 8 + .../GreenField/GreenfieldInvoiceController.cs | 134 ++++---------- ...GreenfieldStorePaymentMethodsController.cs | 4 +- .../GreenField/LocalBTCPayServerClient.cs | 6 +- .../Extensions/AuthorizationExtensions.cs | 6 + .../Bitcoin/BitcoinLikePaymentHandler.cs | 5 +- .../Bitcoin/BitcoinPaymentPromptDetails.cs | 7 + .../Payments/IPaymentMethodHandler.cs | 6 + .../swagger/v1/swagger.template.invoices.json | 170 +++++++++++------- 10 files changed, 185 insertions(+), 169 deletions(-) diff --git a/BTCPayServer.Client/BTCPayServerClient.Invoices.cs b/BTCPayServer.Client/BTCPayServerClient.Invoices.cs index 2e201beea..ee58c8bb4 100644 --- a/BTCPayServer.Client/BTCPayServerClient.Invoices.cs +++ b/BTCPayServer.Client/BTCPayServerClient.Invoices.cs @@ -46,9 +46,15 @@ public partial class BTCPayServerClient return await SendHttpRequest($"api/v1/stores/{storeId}/invoices/{invoiceId}", null, HttpMethod.Get, token); } public virtual async Task GetInvoicePaymentMethods(string storeId, string invoiceId, + bool onlyAccountedPayments = true, bool includeSensitive = false, CancellationToken token = default) { - return await SendHttpRequest($"api/v1/stores/{storeId}/invoices/{invoiceId}/payment-methods", null, HttpMethod.Get, token); + var queryPayload = new Dictionary + { + { nameof(onlyAccountedPayments), onlyAccountedPayments }, + { nameof(includeSensitive), includeSensitive } + }; + return await SendHttpRequest($"api/v1/stores/{storeId}/invoices/{invoiceId}/payment-methods", queryPayload, HttpMethod.Get, token); } public virtual async Task ArchiveInvoice(string storeId, string invoiceId, diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs index 987f8a660..3d306d4ca 100644 --- a/BTCPayServer.Tests/GreenfieldAPITests.cs +++ b/BTCPayServer.Tests/GreenfieldAPITests.cs @@ -2421,6 +2421,14 @@ namespace BTCPayServer.Tests invoice = await client.CreateInvoice(user.StoreId, new CreateInvoiceRequest { Amount = 5000.0m, Currency = "USD" }); methods = await client.GetInvoicePaymentMethods(user.StoreId, invoice.Id); method = methods.First(); + Assert.Equal(JTokenType.Null, method.AdditionalData["accountDerivation"].Type); + Assert.NotNull(method.AdditionalData["keyPath"]); + + methods = await client.GetInvoicePaymentMethods(user.StoreId, invoice.Id, includeSensitive: true); + method = methods.First(); + Assert.Equal(JTokenType.String, method.AdditionalData["accountDerivation"].Type); + var clientViewOnly = await user.CreateClient(Policies.CanViewInvoices); + await AssertApiError(403, "missing-permission", () => clientViewOnly.GetInvoicePaymentMethods(user.StoreId, invoice.Id, includeSensitive: true)); await tester.WaitForEvent(async () => { diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldInvoiceController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldInvoiceController.cs index 961847cde..c16f186c3 100644 --- a/BTCPayServer/Controllers/GreenField/GreenfieldInvoiceController.cs +++ b/BTCPayServer/Controllers/GreenField/GreenfieldInvoiceController.cs @@ -1,6 +1,7 @@ #nullable enable using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -14,6 +15,7 @@ using BTCPayServer.Payments; using BTCPayServer.Payments.Bitcoin; using BTCPayServer.Payouts; using BTCPayServer.Rating; +using BTCPayServer.Security; using BTCPayServer.Security.Greenfield; using BTCPayServer.Services; using BTCPayServer.Services.Invoices; @@ -25,6 +27,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; using Microsoft.CodeAnalysis.CSharp.Syntax; using NBitcoin; +using NBitpayClient; using Newtonsoft.Json.Linq; using CreateInvoiceRequest = BTCPayServer.Client.Models.CreateInvoiceRequest; using InvoiceData = BTCPayServer.Client.Models.InvoiceData; @@ -96,11 +99,7 @@ namespace BTCPayServer.Controllers.Greenfield [FromQuery] int? take = null ) { - var store = HttpContext.GetStoreData(); - if (store == null) - { - return StoreNotFound(); - } + var store = HttpContext.GetStoreData()!; if (startDate is DateTimeOffset s && endDate is DateTimeOffset e && s > e) @@ -133,17 +132,9 @@ namespace BTCPayServer.Controllers.Greenfield [HttpGet("~/api/v1/stores/{storeId}/invoices/{invoiceId}")] public async Task GetInvoice(string storeId, string invoiceId) { - var store = HttpContext.GetStoreData(); - if (store == null) - { - return InvoiceNotFound(); - } - var invoice = await _invoiceRepository.GetInvoice(invoiceId, true); - if (invoice?.StoreId != store.Id) - { + if (!BelongsToThisStore(invoice)) return InvoiceNotFound(); - } return Ok(ToModel(invoice)); } @@ -153,16 +144,9 @@ namespace BTCPayServer.Controllers.Greenfield [HttpDelete("~/api/v1/stores/{storeId}/invoices/{invoiceId}")] public async Task ArchiveInvoice(string storeId, string invoiceId) { - var store = HttpContext.GetStoreData(); - if (store == null) - { - return InvoiceNotFound(); - } var invoice = await _invoiceRepository.GetInvoice(invoiceId, true); - if (invoice?.StoreId != store.Id) - { + if (!BelongsToThisStore(invoice)) return InvoiceNotFound(); - } await _invoiceRepository.ToggleInvoiceArchival(invoiceId, true, storeId); return Ok(); } @@ -172,19 +156,10 @@ namespace BTCPayServer.Controllers.Greenfield [HttpPut("~/api/v1/stores/{storeId}/invoices/{invoiceId}")] public async Task UpdateInvoice(string storeId, string invoiceId, UpdateInvoiceRequest request) { - var store = HttpContext.GetStoreData(); - if (store == null) - { - return InvoiceNotFound(); - } - var result = await _invoiceRepository.UpdateInvoiceMetadata(invoiceId, storeId, request.Metadata); - if (result != null) - { - return Ok(ToModel(result)); - } - - return InvoiceNotFound(); + if (!BelongsToThisStore(result)) + return InvoiceNotFound(); + return Ok(ToModel(result)); } [Authorize(Policy = Policies.CanCreateInvoice, @@ -192,12 +167,7 @@ namespace BTCPayServer.Controllers.Greenfield [HttpPost("~/api/v1/stores/{storeId}/invoices")] public async Task CreateInvoice(string storeId, CreateInvoiceRequest request) { - var store = HttpContext.GetStoreData(); - if (store == null) - { - return StoreNotFound(); - } - + var store = HttpContext.GetStoreData()!; if (request.Amount < 0.0m) { ModelState.AddModelError(nameof(request.Amount), "The amount should be 0 or more."); @@ -271,17 +241,9 @@ namespace BTCPayServer.Controllers.Greenfield public async Task MarkInvoiceStatus(string storeId, string invoiceId, MarkInvoiceStatusRequest request) { - var store = HttpContext.GetStoreData(); - if (store == null) - { - return InvoiceNotFound(); - } - var invoice = await _invoiceRepository.GetInvoice(invoiceId, true); - if (invoice.StoreId != store.Id) - { + if (!BelongsToThisStore(invoice)) return InvoiceNotFound(); - } if (!await _invoiceRepository.MarkInvoiceStatus(invoice.Id, request.Status)) { @@ -300,17 +262,9 @@ namespace BTCPayServer.Controllers.Greenfield [HttpPost("~/api/v1/stores/{storeId}/invoices/{invoiceId}/unarchive")] public async Task UnarchiveInvoice(string storeId, string invoiceId) { - var store = HttpContext.GetStoreData(); - if (store == null) - { - return InvoiceNotFound(); - } - var invoice = await _invoiceRepository.GetInvoice(invoiceId, true); - if (invoice.StoreId != store.Id) - { + if (!BelongsToThisStore(invoice)) return InvoiceNotFound(); - } if (!invoice.Archived) { @@ -328,21 +282,23 @@ namespace BTCPayServer.Controllers.Greenfield [Authorize(Policy = Policies.CanViewInvoices, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] [HttpGet("~/api/v1/stores/{storeId}/invoices/{invoiceId}/payment-methods")] - public async Task GetInvoicePaymentMethods(string storeId, string invoiceId, bool onlyAccountedPayments = true) + public async Task GetInvoicePaymentMethods(string storeId, string invoiceId, bool onlyAccountedPayments = true, bool includeSensitive = false) { - var store = HttpContext.GetStoreData(); - if (store == null) - { - return InvoiceNotFound(); - } - var invoice = await _invoiceRepository.GetInvoice(invoiceId, true); - if (invoice?.StoreId != store.Id) - { + if (!BelongsToThisStore(invoice)) return InvoiceNotFound(); - } - return Ok(ToPaymentMethodModels(invoice, onlyAccountedPayments)); + if (includeSensitive && !await _authorizationService.CanModifyStore(User)) + return this.CreateAPIPermissionError(Policies.CanModifyStoreSettings); + + return Ok(ToPaymentMethodModels(invoice, onlyAccountedPayments, includeSensitive)); + } + + bool BelongsToThisStore([NotNullWhen(true)] InvoiceEntity invoice) => BelongsToThisStore(invoice, out _); + private bool BelongsToThisStore([NotNullWhen(true)] InvoiceEntity invoice, [MaybeNullWhen(false)] out Data.StoreData store) + { + store = this.HttpContext.GetStoreData(); + return invoice?.StoreId is not null && store.Id == invoice.StoreId; } [Authorize(Policy = Policies.CanViewInvoices, @@ -350,17 +306,9 @@ namespace BTCPayServer.Controllers.Greenfield [HttpPost("~/api/v1/stores/{storeId}/invoices/{invoiceId}/payment-methods/{paymentMethod}/activate")] public async Task ActivateInvoicePaymentMethod(string storeId, string invoiceId, string paymentMethod) { - var store = HttpContext.GetStoreData(); - if (store == null) - { - return InvoiceNotFound(); - } - var invoice = await _invoiceRepository.GetInvoice(invoiceId, true); - if (invoice?.StoreId != store.Id) - { + if (!BelongsToThisStore(invoice)) return InvoiceNotFound(); - } if (PaymentMethodId.TryParse(paymentMethod, out var paymentMethodId)) { @@ -381,22 +329,9 @@ namespace BTCPayServer.Controllers.Greenfield CancellationToken cancellationToken = default ) { - var store = HttpContext.GetStoreData(); - if (store == null) - { - return StoreNotFound(); - } - var invoice = await _invoiceRepository.GetInvoice(invoiceId, true); - if (invoice == null) - { + if (!BelongsToThisStore(invoice, out var store)) return InvoiceNotFound(); - } - - if (invoice.StoreId != store.Id) - { - return InvoiceNotFound(); - } if (!invoice.GetInvoiceState().CanRefund()) { return this.CreateAPIError("non-refundable", "Cannot refund this invoice"); @@ -588,12 +523,8 @@ namespace BTCPayServer.Controllers.Greenfield { return this.CreateAPIError(404, "invoice-not-found", "The invoice was not found"); } - private IActionResult StoreNotFound() - { - return this.CreateAPIError(404, "store-not-found", "The store was not found"); - } - private InvoicePaymentMethodDataModel[] ToPaymentMethodModels(InvoiceEntity entity, bool includeAccountedPaymentOnly) + private InvoicePaymentMethodDataModel[] ToPaymentMethodModels(InvoiceEntity entity, bool includeAccountedPaymentOnly, bool includeSensitive) { return entity.GetPaymentPrompts().Select( prompt => @@ -606,7 +537,12 @@ namespace BTCPayServer.Controllers.Greenfield var details = prompt.Details; if (handler is not null && prompt.Activated) - details = JToken.FromObject(handler.ParsePaymentPromptDetails(details), handler.Serializer.ForAPI()); + { + var detailsObj = handler.ParsePaymentPromptDetails(details); + if (!includeSensitive) + handler.StripDetailsForNonOwner(detailsObj); + details = JToken.FromObject(detailsObj, handler.Serializer.ForAPI()); + } return new InvoicePaymentMethodDataModel { Activated = prompt.Activated, @@ -621,7 +557,7 @@ namespace BTCPayServer.Controllers.Greenfield PaymentMethodFee = accounting?.PaymentMethodFee ?? 0m, PaymentLink = (prompt.Activated ? paymentLinkExtension?.GetPaymentLink(prompt, Url) : null) ?? string.Empty, Payments = payments.Select(paymentEntity => ToPaymentModel(entity, paymentEntity)).ToList(), - AdditionalData = prompt.Details + AdditionalData = details }; }).ToArray(); } diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldStorePaymentMethodsController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldStorePaymentMethodsController.cs index 41cf051d0..cc02455d0 100644 --- a/BTCPayServer/Controllers/GreenField/GreenfieldStorePaymentMethodsController.cs +++ b/BTCPayServer/Controllers/GreenField/GreenfieldStorePaymentMethodsController.cs @@ -145,9 +145,7 @@ namespace BTCPayServer.Controllers.Greenfield if (includeConfig is true) { - var canModifyStore = (await _authorizationService.AuthorizeAsync(User, null, - new PolicyRequirement(Policies.CanModifyStoreSettings))).Succeeded; - if (!canModifyStore) + if (!await _authorizationService.CanModifyStore(User)) return this.CreateAPIPermissionError(Policies.CanModifyStoreSettings); } diff --git a/BTCPayServer/Controllers/GreenField/LocalBTCPayServerClient.cs b/BTCPayServer/Controllers/GreenField/LocalBTCPayServerClient.cs index 1babe8558..b647eda4e 100644 --- a/BTCPayServer/Controllers/GreenField/LocalBTCPayServerClient.cs +++ b/BTCPayServer/Controllers/GreenField/LocalBTCPayServerClient.cs @@ -831,10 +831,12 @@ namespace BTCPayServer.Controllers.Greenfield } public override async Task GetInvoicePaymentMethods(string storeId, - string invoiceId, CancellationToken token = default) + string invoiceId, + bool onlyAccountedPayments = true, bool includeSensitive = false, + CancellationToken token = default) { return GetFromActionResult( - await GetController().GetInvoicePaymentMethods(storeId, invoiceId)); + await GetController().GetInvoicePaymentMethods(storeId, invoiceId, onlyAccountedPayments, includeSensitive)); } public override async Task ArchiveInvoice(string storeId, string invoiceId, CancellationToken token = default) diff --git a/BTCPayServer/Extensions/AuthorizationExtensions.cs b/BTCPayServer/Extensions/AuthorizationExtensions.cs index da1bc4f7b..b3ac9108e 100644 --- a/BTCPayServer/Extensions/AuthorizationExtensions.cs +++ b/BTCPayServer/Extensions/AuthorizationExtensions.cs @@ -2,6 +2,7 @@ using System.Security.Claims; using System.Threading.Tasks; using BTCPayServer.Abstractions.Constants; using BTCPayServer.Client; +using BTCPayServer.Security; using BTCPayServer.Security.Bitpay; using BTCPayServer.Security.Greenfield; using BTCPayServer.Services; @@ -12,6 +13,11 @@ namespace BTCPayServer { public static class AuthorizationExtensions { + public static async Task CanModifyStore(this IAuthorizationService authorizationService, ClaimsPrincipal user) + { + return (await authorizationService.AuthorizeAsync(user, null, + new PolicyRequirement(Policies.CanModifyStoreSettings))).Succeeded; + } public static async Task<(bool HotWallet, bool RPCImport)> CanUseHotWallet( this IAuthorizationService authorizationService, PoliciesSettings policiesSettings, diff --git a/BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentHandler.cs b/BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentHandler.cs index 10511b330..de4a1a227 100644 --- a/BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentHandler.cs +++ b/BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentHandler.cs @@ -89,7 +89,10 @@ namespace BTCPayServer.Payments.Bitcoin { return ParsePaymentMethodConfig(config); } - + public void StripDetailsForNonOwner(object details) + { + ((BitcoinPaymentPromptDetails)details).AccountDerivation = null; + } public async Task AfterSavingInvoice(PaymentMethodContext paymentMethodContext) { var paymentPrompt = paymentMethodContext.Prompt; diff --git a/BTCPayServer/Payments/Bitcoin/BitcoinPaymentPromptDetails.cs b/BTCPayServer/Payments/Bitcoin/BitcoinPaymentPromptDetails.cs index 18f5a1c95..8bf9b2ca2 100644 --- a/BTCPayServer/Payments/Bitcoin/BitcoinPaymentPromptDetails.cs +++ b/BTCPayServer/Payments/Bitcoin/BitcoinPaymentPromptDetails.cs @@ -14,6 +14,9 @@ namespace BTCPayServer.Payments.Bitcoin [JsonConverter(typeof(StringEnumConverter))] public NetworkFeeMode FeeMode { get; set; } + /// + /// The fee rate charged to the user as `PaymentMethodFee`. + /// [JsonConverter(typeof(NBitcoin.JsonConverters.FeeRateJsonConverter))] public FeeRate PaymentMethodFeeRate { @@ -21,6 +24,10 @@ namespace BTCPayServer.Payments.Bitcoin set; } public bool PayjoinEnabled { get; set; } + + /// + /// The recommended fee rate for this payment method. + /// [JsonConverter(typeof(NBitcoin.JsonConverters.FeeRateJsonConverter))] public FeeRate RecommendedFeeRate { get; set; } [JsonConverter(typeof(NBitcoin.JsonConverters.KeyPathJsonConverter))] diff --git a/BTCPayServer/Payments/IPaymentMethodHandler.cs b/BTCPayServer/Payments/IPaymentMethodHandler.cs index 48c454bf3..b318e7856 100644 --- a/BTCPayServer/Payments/IPaymentMethodHandler.cs +++ b/BTCPayServer/Payments/IPaymentMethodHandler.cs @@ -67,6 +67,12 @@ namespace BTCPayServer.Payments /// /// object ParsePaymentPromptDetails(JToken details); + /// + /// Remove properties from the details which shouldn't appear to non-store owner. + /// + /// Prompt details + void StripDetailsForNonOwner(object details) { } + /// /// Parse the configuration of the payment method in the store /// diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.invoices.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.invoices.json index 2463c0003..2f041fce6 100644 --- a/BTCPayServer/wwwroot/swagger/v1/swagger.template.invoices.json +++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.invoices.json @@ -413,6 +413,16 @@ "type": "boolean", "default": true } + }, + { + "name": "includeSensitive", + "in": "query", + "required": false, + "description": "If `true`, `additionalData` might include sensitive data (such as xpub). Requires the permission `btcpay.store.canmodifystoresettings`.", + "schema": { + "type": "boolean", + "default": false + } } ], "description": "View information about the specified invoice's payment methods", @@ -644,10 +654,10 @@ ] } }, - "/api/v1/stores/{storeId}/invoices/{invoiceId}/refund": { + "/api/v1/stores/{storeId}/invoices/{invoiceId}/refund": { "post": { "tags": [ - "Invoices" + "Invoices" ], "summary": "Refund invoice", "parameters": [ @@ -668,69 +678,69 @@ "schema": { "type": "string" } - } - ], - "description": "Refund invoice", - "operationId": "Invoices_Refund", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": false, - "properties": { - "name": { - "type": "string", - "description": "Name of the pull payment (Default: 'Refund' followed by the invoice id)", - "nullable": true - }, - "description": { - "type": "string", - "description": "Description of the pull payment" - }, - "payoutMethodId": { - "$ref": "#/components/schemas/PayoutMethodId" - }, - "refundVariant": { - "type": "string", - "description": "* `RateThen`: Refund the crypto currency price, at the rate the invoice got paid.\r\n* `CurrentRate`: Refund the crypto currency price, at the current rate.\r\n*`Fiat`: Refund the invoice currency, at the rate when the refund will be sent.\r\n*`OverpaidAmount`: Refund the crypto currency amount that was overpaid.\r\n*`Custom`: Specify the amount, currency, and rate of the refund. (see `customAmount` and `customCurrency`)", - "x-enumNames": [ - "RateThen", - "CurrentRate", - "Fiat", - "Custom" - ], - "enum": [ - "RateThen", - "CurrentRate", - "OverpaidAmount", - "Fiat", - "Custom" - ] - }, - "subtractPercentage": { - "type": "string", - "format": "decimal", - "description": "Optional percentage by which to reduce the refund, e.g. as processing charge or to compensate for the mining fee.", - "example": "2.1" - }, - "customAmount": { - "type": "string", - "format": "decimal", - "description": "The amount to refund if the `refundVariant` is `Custom`.", - "example": "5.00" - }, - "customCurrency": { - "type": "string", - "description": "The currency to refund if the `refundVariant` is `Custom`", - "example": "USD" - } - } - } + } + ], + "description": "Refund invoice", + "operationId": "Invoices_Refund", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "description": "Name of the pull payment (Default: 'Refund' followed by the invoice id)", + "nullable": true + }, + "description": { + "type": "string", + "description": "Description of the pull payment" + }, + "payoutMethodId": { + "$ref": "#/components/schemas/PayoutMethodId" + }, + "refundVariant": { + "type": "string", + "description": "* `RateThen`: Refund the crypto currency price, at the rate the invoice got paid.\r\n* `CurrentRate`: Refund the crypto currency price, at the current rate.\r\n*`Fiat`: Refund the invoice currency, at the rate when the refund will be sent.\r\n*`OverpaidAmount`: Refund the crypto currency amount that was overpaid.\r\n*`Custom`: Specify the amount, currency, and rate of the refund. (see `customAmount` and `customCurrency`)", + "x-enumNames": [ + "RateThen", + "CurrentRate", + "Fiat", + "Custom" + ], + "enum": [ + "RateThen", + "CurrentRate", + "OverpaidAmount", + "Fiat", + "Custom" + ] + }, + "subtractPercentage": { + "type": "string", + "format": "decimal", + "description": "Optional percentage by which to reduce the refund, e.g. as processing charge or to compensate for the mining fee.", + "example": "2.1" + }, + "customAmount": { + "type": "string", + "format": "decimal", + "description": "The amount to refund if the `refundVariant` is `Custom`.", + "example": "5.00" + }, + "customCurrency": { + "type": "string", + "description": "The currency to refund if the `refundVariant` is `Custom`", + "example": "USD" + } + } + } } } - }, + }, "responses": { "200": { "description": "Pull payment for refunding the invoice", @@ -1329,6 +1339,7 @@ "anyOf": [ { "type": "object", + "title": "*-LNURL", "description": "LNURL Pay information", "properties": { "providedComment": { @@ -1345,6 +1356,39 @@ } } }, + { + "type": "object", + "title": "*-CHAIN", + "description": "Bitcoin On-Chain payment information", + "properties": { + "keyPath": { + "type": "string", + "description": "The key path relative to the account derviation key.", + "example": "0/1" + }, + "payjoinEnabled": { + "type": "boolean", + "description": "If the payjoin feature is enabled for this payment method." + }, + "accountDerivation": { + "type": "string", + "description": "The derivation scheme used to derive addresses (null if `includeSensitive` is `false`)", + "example": "xpub6DVMcQAQCtGbNDTEjQGtR1GRoTKw7AzP6bVivX4gFnewcnRk1r1tbczpfsaYjKKVrmtyiwYqAEnALYzZ8yoTArVsKfZekmwLFqQp4MRgPhy" + }, + "recommendedFeeRate": { + "type": "string", + "format": "decimal", + "description": "The recommended fee rate for this payment method.", + "example": "4.107" + }, + "paymentMethodFeeRate": { + "type": "string", + "format": "decimal", + "description": "The fee rate charged to the user as `PaymentMethodFee`.", + "example": "3.975" + } + } + }, { "type": "object", "description": "No additional information"