Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 58 additions & 8 deletions btcpayserver/Minmo.BTCPayServer.Plugin/MinmoLightningClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ValidationResult?> Validate()
Expand Down Expand Up @@ -90,7 +90,7 @@ public async Task<LightningInvoice> 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);
Expand Down Expand Up @@ -224,8 +224,8 @@ private async Task<JObject> 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));
Expand All @@ -240,23 +240,73 @@ private async Task<JObject> 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<string>("BOLT11");
var paymentHash = NormalizePaymentHash(json.Value<string>("paymentHash"), bolt11);
return new LightningInvoice
{
Id = json.Value<string>("id"),
Id = paymentHash ?? json.Value<string>("id"),
Status = ParseInvoiceStatus(json.Value<string>("status")),
BOLT11 = json.Value<string>("BOLT11"),
PaymentHash = json.Value<string>("paymentHash"),
BOLT11 = bolt11,
PaymentHash = paymentHash,
ExpiresAt = json.Value<DateTimeOffset?>("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;
}
Comment on lines +267 to +282

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<JArray>())
{
if (entry.Count >= 2 &&
string.Equals(entry[0].Value<string>(), "text/plain", StringComparison.OrdinalIgnoreCase))
{
return entry[1].Value<string>() ?? string.Empty;
}
}
}
catch (JsonReaderException)
{
}

Comment on lines +303 to +306
return description;
}

private static NodeInfo[] ParseNodeInfoList(JObject json)
{
var nodeUris = json["nodeURIs"] as JArray;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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)
Expand Down
8 changes: 4 additions & 4 deletions btcpayserver/Minmo.BTCPayServer.Plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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=<team-id>;wallet-id=<wallet-id>;api-token=<token>
type=minmo;server=https://escrow.example.com;token=<connection-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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
</p>
<ul class="mb-0">
<li>
<code><b>type=</b>minmo;<b>server=</b>https://wallet.example.com;<b>team-id=</b>...;<b>wallet-id=</b>...;<b>api-token=</b>mmo_btcpay_...</code>
<code><b>type=</b>minmo;<b>server=</b>https://wallet.example.com;<b>token=</b>...</code>
</li>
</ul>
</div>
Expand Down