diff --git a/btcpayserver/Minmo.BTCPayServer.Plugin/MinmoLightningClient.cs b/btcpayserver/Minmo.BTCPayServer.Plugin/MinmoLightningClient.cs index aee311a..c5ad694 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() @@ -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); @@ -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)); @@ -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; 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=...