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;