From 4f73698dee7b6b47c18839336ccd0a185a3e4d86 Mon Sep 17 00:00:00 2001 From: okjodom Date: Fri, 10 Jul 2026 17:03:10 +0300 Subject: [PATCH 1/2] Use opaque Minmo BTCPay connection token --- .../Minmo.BTCPayServer.Plugin/MinmoLightningClient.cs | 6 +++--- .../MinmoLightningConnectionString.cs | 8 ++------ btcpayserver/Minmo.BTCPayServer.Plugin/README.md | 8 ++++---- .../Views/MinmoLightningNodeHelp.cshtml | 2 +- 4 files changed, 10 insertions(+), 14 deletions(-) diff --git a/btcpayserver/Minmo.BTCPayServer.Plugin/MinmoLightningClient.cs b/btcpayserver/Minmo.BTCPayServer.Plugin/MinmoLightningClient.cs index aee311a..cc7dd52 100644 --- a/btcpayserver/Minmo.BTCPayServer.Plugin/MinmoLightningClient.cs +++ b/btcpayserver/Minmo.BTCPayServer.Plugin/MinmoLightningClient.cs @@ -35,7 +35,7 @@ public MinmoLightningClient( public override string ToString() { - return $"type=minmo;server={_connection.Server};team-id={_connection.TeamId};wallet-id={_connection.WalletId};api-token=***"; + return $"type=minmo;server={_connection.Server};token=***"; } public async Task Validate() @@ -224,8 +224,8 @@ private async Task SendJson( CancellationToken cancellation) { using var request = new HttpRequestMessage(method, _connection.BuildUri( - $"/v1/btcpay/teams/{Uri.EscapeDataString(_connection.TeamId)}/wallets/{Uri.EscapeDataString(_connection.WalletId)}/{operation}")); - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _connection.ApiToken); + $"/v1/btcpay/wallet/{operation}")); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _connection.Token); if (payload is not null) { request.Content = new StringContent(payload.ToString(Formatting.None)); diff --git a/btcpayserver/Minmo.BTCPayServer.Plugin/MinmoLightningConnectionString.cs b/btcpayserver/Minmo.BTCPayServer.Plugin/MinmoLightningConnectionString.cs index cdf0fbe..bc61ca9 100644 --- a/btcpayserver/Minmo.BTCPayServer.Plugin/MinmoLightningConnectionString.cs +++ b/btcpayserver/Minmo.BTCPayServer.Plugin/MinmoLightningConnectionString.cs @@ -2,9 +2,7 @@ namespace Minmo.BTCPayServer.Plugin; internal sealed record MinmoLightningConnectionString( Uri Server, - string TeamId, - string WalletId, - string ApiToken) + string Token) { public const string Type = "minmo"; @@ -26,9 +24,7 @@ serverUri.Scheme is not ("http" or "https")) return new MinmoLightningConnectionString( TrimServer(serverUri), - Required(values, "team-id"), - Required(values, "wallet-id"), - Required(values, "api-token")); + Required(values, "token")); } private static Uri TrimServer(Uri server) diff --git a/btcpayserver/Minmo.BTCPayServer.Plugin/README.md b/btcpayserver/Minmo.BTCPayServer.Plugin/README.md index b9f02c0..8f7f93b 100644 --- a/btcpayserver/Minmo.BTCPayServer.Plugin/README.md +++ b/btcpayserver/Minmo.BTCPayServer.Plugin/README.md @@ -12,12 +12,12 @@ Paste the connection string generated from the Minmo wallet page into BTCPay Ser custom Lightning node setup: ```text -type=minmo;server=https://escrow.example.com;team-id=;wallet-id=;api-token= +type=minmo;server=https://escrow.example.com;token= ``` -The token should be generated by Minmo and is stored hashed in the Minmo team wallet -configuration. The BTCPay plugin sends it as a bearer token when calling the Minmo -wallet facade. +The token is generated by Minmo and contains the wallet routing information and +access token in a compact JWT envelope. The BTCPay plugin treats it as opaque and +sends it as a bearer token when calling the Minmo wallet facade. ## Development diff --git a/btcpayserver/Minmo.BTCPayServer.Plugin/Views/MinmoLightningNodeHelp.cshtml b/btcpayserver/Minmo.BTCPayServer.Plugin/Views/MinmoLightningNodeHelp.cshtml index 47f32e1..08cb360 100644 --- a/btcpayserver/Minmo.BTCPayServer.Plugin/Views/MinmoLightningNodeHelp.cshtml +++ b/btcpayserver/Minmo.BTCPayServer.Plugin/Views/MinmoLightningNodeHelp.cshtml @@ -17,7 +17,7 @@

  • - type=minmo;server=https://wallet.example.com;team-id=...;wallet-id=...;api-token=mmo_btcpay_... + type=minmo;server=https://wallet.example.com;token=...
From f839bc4ff2cc589d5f15ab5b80fbe5c3a3ba8681 Mon Sep 17 00:00:00 2001 From: okjodom Date: Fri, 10 Jul 2026 19:21:16 +0300 Subject: [PATCH 2/2] Fix Minmo Lightning payment hash handling --- .../MinmoLightningClient.cs | 60 +++++++++++++++++-- 1 file changed, 55 insertions(+), 5 deletions(-) diff --git a/btcpayserver/Minmo.BTCPayServer.Plugin/MinmoLightningClient.cs b/btcpayserver/Minmo.BTCPayServer.Plugin/MinmoLightningClient.cs index cc7dd52..c5ad694 100644 --- a/btcpayserver/Minmo.BTCPayServer.Plugin/MinmoLightningClient.cs +++ b/btcpayserver/Minmo.BTCPayServer.Plugin/MinmoLightningClient.cs @@ -90,7 +90,7 @@ public async Task CreateInvoice( ["amountSats"] = invoice.Amount .ToUnit(LightMoneyUnit.Satoshi) .ToString("0", CultureInfo.InvariantCulture), - ["description"] = invoice.Description ?? string.Empty, + ["description"] = NormalizeDescription(invoice.Description), ["expiry"] = Math.Max(1, (int)invoice.Expiry.TotalSeconds) }; var json = await SendJson(HttpMethod.Post, "invoices", payload, cancellation); @@ -240,23 +240,73 @@ private async Task SendJson( $"Minmo wallet request failed with {(int)response.StatusCode}: {responseBody}"); } - return JObject.Parse(responseBody); + using var stringReader = new StringReader(responseBody); + using var jsonReader = new JsonTextReader(stringReader) + { + DateParseHandling = DateParseHandling.DateTimeOffset + }; + return JObject.Load(jsonReader); } private LightningInvoice ToLightningInvoice(JObject json) { + var bolt11 = json.Value("BOLT11"); + var paymentHash = NormalizePaymentHash(json.Value("paymentHash"), bolt11); return new LightningInvoice { - Id = json.Value("id"), + Id = paymentHash ?? json.Value("id"), Status = ParseInvoiceStatus(json.Value("status")), - BOLT11 = json.Value("BOLT11"), - PaymentHash = json.Value("paymentHash"), + BOLT11 = bolt11, + PaymentHash = paymentHash, ExpiresAt = json.Value("expiresAt") ?? DateTimeOffset.UtcNow.AddMinutes(5), Amount = ParseLightMoney(json["amount"]), AmountReceived = ParseLightMoney(json["amountReceived"]) }; } + private string? NormalizePaymentHash(string? paymentHash, string? bolt11) + { + if (!string.IsNullOrWhiteSpace(paymentHash) && + uint256.TryParse(paymentHash, out var parsedPaymentHash)) + { + return parsedPaymentHash.ToString(); + } + + if (!string.IsNullOrWhiteSpace(bolt11) && + BOLT11PaymentRequest.TryParse(bolt11, out var paymentRequest, _network)) + { + return paymentRequest?.PaymentHash?.ToString(); + } + + return null; + } + + private static string NormalizeDescription(string? description) + { + if (string.IsNullOrWhiteSpace(description)) + { + return string.Empty; + } + + try + { + var metadata = JArray.Parse(description); + foreach (var entry in metadata.OfType()) + { + if (entry.Count >= 2 && + string.Equals(entry[0].Value(), "text/plain", StringComparison.OrdinalIgnoreCase)) + { + return entry[1].Value() ?? string.Empty; + } + } + } + catch (JsonReaderException) + { + } + + return description; + } + private static NodeInfo[] ParseNodeInfoList(JObject json) { var nodeUris = json["nodeURIs"] as JArray;