├── .gitignore
├── .github
├── FUNDING.yml
└── workflows
│ └── ci.yml
├── LNURL
├── LNUrlException.cs
├── LNURLHostedChannelRequest.cs
├── LNUrlStatusResponse.cs
├── JsonConverters
│ ├── NodeUriJsonConverter.cs
│ ├── PubKeyJsonConverter.cs
│ ├── SigJsonConverter.cs
│ └── UriJsonConverter.cs
├── LNURL.csproj
├── Extensions.cs
├── LNURLChannelRequest.cs
├── LNURLWithdrawRequest.cs
├── LNAuthRequest.cs
├── LNURL.cs
├── Bech32.cs
├── BoltCardHelper.cs
└── LNURLPayRequest.cs
├── LICENSE
├── LNURL.Tests
├── LNURL.Tests.csproj
└── UnitTest1.cs
└── LNURL.sln
/.gitignore:
--------------------------------------------------------------------------------
1 | bin/
2 | obj/
3 | /packages/
4 | riderModule.iml
5 | /_ReSharper.Caches/
6 | .idea
7 | LNURL.sln.DotSettings.user
8 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: [kukks]
4 | custom: ['https://donate.kukks.org']
5 |
--------------------------------------------------------------------------------
/LNURL/LNUrlException.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace LNURL;
4 |
5 | public class LNUrlException : Exception
6 | {
7 | public LNUrlException(string message) : base(message)
8 | {
9 | }
10 | }
--------------------------------------------------------------------------------
/LNURL/LNURLHostedChannelRequest.cs:
--------------------------------------------------------------------------------
1 | using BTCPayServer.Lightning;
2 | using LNURL.JsonConverters;
3 | using Newtonsoft.Json;
4 |
5 | namespace LNURL;
6 |
7 | ///
8 | /// https://github.com/fiatjaf/lnurl-rfc/blob/luds/07.md
9 | ///
10 | public class LNURLHostedChannelRequest
11 | {
12 | [JsonProperty("uri")]
13 | [JsonConverter(typeof(NodeUriJsonConverter))]
14 | public NodeInfo Uri { get; set; }
15 |
16 | [JsonProperty("alias")] public string Alias { get; set; }
17 |
18 | [JsonProperty("k1")] public string K1 { get; set; }
19 |
20 | [JsonProperty("tag")] public string Tag { get; set; }
21 | }
--------------------------------------------------------------------------------
/LNURL/LNUrlStatusResponse.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Newtonsoft.Json;
3 | using Newtonsoft.Json.Linq;
4 |
5 | namespace LNURL;
6 |
7 | public class LNUrlStatusResponse
8 | {
9 | [JsonProperty("status")] public string Status { get; set; }
10 | [JsonProperty("reason")] public string Reason { get; set; }
11 |
12 | public static bool IsErrorResponse(JObject response, out LNUrlStatusResponse status)
13 | {
14 | if (response.ContainsKey("status") && response["status"].Value()
15 | .Equals("Error", StringComparison.InvariantCultureIgnoreCase))
16 | {
17 | status = response.ToObject();
18 | return true;
19 | }
20 |
21 | status = null;
22 | return false;
23 | }
24 | }
--------------------------------------------------------------------------------
/LNURL/JsonConverters/NodeUriJsonConverter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics.CodeAnalysis;
3 | using BTCPayServer.Lightning;
4 | using NBitcoin.JsonConverters;
5 | using Newtonsoft.Json;
6 |
7 | namespace LNURL.JsonConverters;
8 |
9 | public class NodeUriJsonConverter : JsonConverter
10 | {
11 | public override NodeInfo ReadJson(JsonReader reader, Type objectType, [AllowNull] NodeInfo existingValue,
12 | bool hasExistingValue, JsonSerializer serializer)
13 | {
14 | if (reader.TokenType != JsonToken.String)
15 | throw new JsonObjectException("Unexpected token type for NodeUri", reader.Path);
16 | if (NodeInfo.TryParse((string) reader.Value, out var info))
17 | return info;
18 | throw new JsonObjectException("Invalid NodeUri", reader.Path);
19 | }
20 |
21 | public override void WriteJson(JsonWriter writer, [AllowNull] NodeInfo value, JsonSerializer serializer)
22 | {
23 | if (value is NodeInfo)
24 | writer.WriteValue(value.ToString());
25 | }
26 | }
--------------------------------------------------------------------------------
/LNURL/LNURL.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | Andrew Camilleri (kukks)
6 | MIT
7 | true
8 | LNURL protocol implementation in .NET Core
9 | bitcoin lightning lnurl
10 | 0.0.36
11 | https://github.com/Kukks/LNURL.git
12 | git
13 | MIT
14 | 10
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/LNURL/JsonConverters/PubKeyJsonConverter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics.CodeAnalysis;
3 | using NBitcoin;
4 | using NBitcoin.JsonConverters;
5 | using Newtonsoft.Json;
6 |
7 | namespace LNURL.JsonConverters;
8 |
9 | public class PubKeyJsonConverter : JsonConverter
10 | {
11 | public override PubKey ReadJson(JsonReader reader, Type objectType, [AllowNull] PubKey existingValue,
12 | bool hasExistingValue, JsonSerializer serializer)
13 | {
14 | if (reader.TokenType != JsonToken.String)
15 | throw new JsonObjectException("Unexpected token type for PubKey", reader.Path);
16 | try
17 | {
18 | return new PubKey((string) reader.Value);
19 | }
20 | catch (Exception e)
21 | {
22 | throw new JsonObjectException(e.Message, reader.Path);
23 | }
24 | }
25 |
26 | public override void WriteJson(JsonWriter writer, [AllowNull] PubKey value, JsonSerializer serializer)
27 | {
28 | if (value is { })
29 | writer.WriteValue(value.ToString());
30 | }
31 | }
--------------------------------------------------------------------------------
/LNURL/Extensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Net;
3 | using NBitcoin;
4 |
5 | namespace LNURL;
6 |
7 | public static class Extensions
8 | {
9 | public static bool IsOnion(this Uri uri)
10 | {
11 | if (uri == null || !uri.IsAbsoluteUri)
12 | return false;
13 | return uri.DnsSafeHost.EndsWith(".onion", StringComparison.OrdinalIgnoreCase);
14 | }
15 |
16 | public static bool IsLocalNetwork(this Uri server)
17 | {
18 | if (server == null)
19 | throw new ArgumentNullException(nameof(server));
20 |
21 | if (server.HostNameType == UriHostNameType.Dns)
22 | return server.Host.EndsWith(".internal", StringComparison.OrdinalIgnoreCase) ||
23 | server.Host.EndsWith(".local", StringComparison.OrdinalIgnoreCase) ||
24 | server.Host.EndsWith(".lan", StringComparison.OrdinalIgnoreCase) ||
25 | server.Host.IndexOf('.', StringComparison.OrdinalIgnoreCase) == -1;
26 |
27 | if (IPAddress.TryParse(server.Host, out var ip)) return ip.IsLocal() || ip.IsRFC1918();
28 |
29 | return false;
30 | }
31 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Andrew Camilleri
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/LNURL/JsonConverters/SigJsonConverter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics.CodeAnalysis;
3 | using NBitcoin.Crypto;
4 | using NBitcoin.DataEncoders;
5 | using NBitcoin.JsonConverters;
6 | using Newtonsoft.Json;
7 |
8 | namespace LNURL.JsonConverters;
9 |
10 | public class SigJsonConverter : JsonConverter
11 | {
12 | public override ECDSASignature ReadJson(JsonReader reader, Type objectType,
13 | [AllowNull] ECDSASignature existingValue, bool hasExistingValue, JsonSerializer serializer)
14 | {
15 | if (reader.TokenType != JsonToken.String)
16 | throw new JsonObjectException("Unexpected token type for ECDSASignature", reader.Path);
17 | try
18 | {
19 | return ECDSASignature.FromDER(Encoders.Hex.DecodeData((string) reader.Value));
20 | }
21 | catch (Exception e)
22 | {
23 | throw new JsonObjectException(e.Message, reader.Path);
24 | }
25 | }
26 |
27 | public override void WriteJson(JsonWriter writer, [AllowNull] ECDSASignature value, JsonSerializer serializer)
28 | {
29 | if (value is { })
30 | writer.WriteValue(Encoders.Hex.EncodeData(value.ToDER()));
31 | }
32 | }
--------------------------------------------------------------------------------
/LNURL.Tests/LNURL.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 |
6 | false
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | runtime; build; native; contentfiles; analyzers; buildtransitive
16 | all
17 |
18 |
19 | runtime; build; native; contentfiles; analyzers; buildtransitive
20 | all
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/LNURL.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LNURL", "LNURL\LNURL.csproj", "{8AE80152-6EF0-47BC-AE09-B9C72DBC6902}"
4 | EndProject
5 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LNURL.Tests", "LNURL.Tests\LNURL.Tests.csproj", "{0D55DB17-C3F5-4FB4-A456-58C3E8593AC2}"
6 | EndProject
7 | Global
8 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
9 | Debug|Any CPU = Debug|Any CPU
10 | Release|Any CPU = Release|Any CPU
11 | EndGlobalSection
12 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
13 | {8AE80152-6EF0-47BC-AE09-B9C72DBC6902}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
14 | {8AE80152-6EF0-47BC-AE09-B9C72DBC6902}.Debug|Any CPU.Build.0 = Debug|Any CPU
15 | {8AE80152-6EF0-47BC-AE09-B9C72DBC6902}.Release|Any CPU.ActiveCfg = Release|Any CPU
16 | {8AE80152-6EF0-47BC-AE09-B9C72DBC6902}.Release|Any CPU.Build.0 = Release|Any CPU
17 | {0D55DB17-C3F5-4FB4-A456-58C3E8593AC2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
18 | {0D55DB17-C3F5-4FB4-A456-58C3E8593AC2}.Debug|Any CPU.Build.0 = Debug|Any CPU
19 | {0D55DB17-C3F5-4FB4-A456-58C3E8593AC2}.Release|Any CPU.ActiveCfg = Release|Any CPU
20 | {0D55DB17-C3F5-4FB4-A456-58C3E8593AC2}.Release|Any CPU.Build.0 = Release|Any CPU
21 | EndGlobalSection
22 | EndGlobal
23 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: 'Publish application'
2 | on:
3 | # Run the build for pushes and pull requests targeting master
4 | push:
5 | branches:
6 | - master
7 | pull_request:
8 | branches:
9 | - master
10 |
11 | jobs:
12 | build:
13 | runs-on: ubuntu-latest
14 | steps:
15 | # Checkout the code
16 | - uses: actions/checkout@v2
17 |
18 | # Install .NET Core SDK
19 | - name: Setup .NET Core
20 | uses: actions/setup-dotnet@v1
21 | with:
22 | dotnet-version: 8.0.x
23 | # Run tests
24 | - name: Test
25 | run: dotnet test
26 | - name: Publish NuGet
27 | if: ${{ github.ref == 'refs/heads/master' }} # Publish only when the push is on master
28 | uses: Rebel028/publish-nuget@v2.7.0
29 | with:
30 | PROJECT_FILE_PATH: LNURL/LNURL.csproj
31 | NUGET_KEY: ${{secrets.NUGET_KEY}}
32 | PACKAGE_NAME: LNURL
33 | INCLUDE_SYMBOLS: false
34 | VERSION_REGEX: ^\s*(.*)<\/PackageVersion>\s*$
35 | TAG_COMMIT: true
36 | TAG_FORMAT: v*
37 |
38 | - name: Publish Github Package Registry
39 | if: ${{ github.ref == 'refs/heads/master' }} # Publish only when the push is on master
40 | uses: Rebel028/publish-nuget@v2.7.0
41 | with:
42 | PROJECT_FILE_PATH: LNURL/LNURL.csproj
43 | NUGET_SOURCE: "https://nuget.pkg.github.com/Kukks"
44 | NUGET_KEY: ${{secrets.GH_TOKEN}}
45 | PACKAGE_NAME: LNURL
46 | INCLUDE_SYMBOLS: false
47 | VERSION_REGEX: ^\s*(.*)<\/PackageVersion>\s*$
48 | TAG_COMMIT: true
49 | TAG_FORMAT: v*
50 |
--------------------------------------------------------------------------------
/LNURL/JsonConverters/UriJsonConverter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Reflection;
3 | using NBitcoin.JsonConverters;
4 | using Newtonsoft.Json;
5 |
6 | namespace LNURL.JsonConverters;
7 |
8 | public class UriJsonConverter : JsonConverter
9 | {
10 | public override bool CanConvert(Type objectType)
11 | {
12 | return typeof(Uri).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo()) || objectType == typeof(string);
13 | }
14 |
15 | public override object ReadJson(JsonReader reader, Type objectType, object existingValue,
16 | JsonSerializer serializer)
17 | {
18 | try
19 | {
20 | var res = reader.TokenType == JsonToken.Null ? null :
21 | reader.TokenType == JsonToken.String && string.IsNullOrEmpty(reader.Value?.ToString()) ? null :
22 | Uri.TryCreate((string) reader.Value, UriKind.Absolute, out var result) ? result :
23 | throw new JsonObjectException("Invalid Uri value", reader);
24 | if (objectType == typeof(string))
25 | {
26 | return res?.ToString();
27 | }
28 |
29 | return res;
30 | }
31 | catch (InvalidCastException)
32 | {
33 | throw new JsonObjectException("Invalid Uri value", reader);
34 | }
35 | }
36 |
37 | public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
38 | {
39 | switch (value)
40 | {
41 | case null:
42 | return;
43 | case string s:
44 | writer.WriteValue(s);
45 | break;
46 | case Uri uri:
47 | writer.WriteValue(uri.IsAbsoluteUri? uri.AbsoluteUri : uri.ToString());
48 | break;
49 | }
50 | }
51 | }
--------------------------------------------------------------------------------
/LNURL/LNURLChannelRequest.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Net.Http;
3 | using System.Threading;
4 | using System.Threading.Tasks;
5 | using BTCPayServer.Lightning;
6 | using LNURL.JsonConverters;
7 | using NBitcoin;
8 | using Newtonsoft.Json;
9 | using Newtonsoft.Json.Linq;
10 |
11 | namespace LNURL;
12 |
13 | ///
14 | /// https://github.com/fiatjaf/lnurl-rfc/blob/luds/02.md
15 | ///
16 | public class LNURLChannelRequest
17 | {
18 | [JsonProperty("uri")]
19 | [JsonConverter(typeof(NodeUriJsonConverter))]
20 | public NodeInfo Uri { get; set; }
21 |
22 | [JsonProperty("callback")]
23 | [JsonConverter(typeof(UriJsonConverter))]
24 | public Uri Callback { get; set; }
25 |
26 | [JsonProperty("k1")] public string K1 { get; set; }
27 |
28 | [JsonProperty("tag")] public string Tag { get; set; }
29 |
30 |
31 | public async Task SendRequest(PubKey ourId, bool privateChannel, HttpClient httpClient,
32 | CancellationToken cancellationToken = default)
33 | {
34 | var url = Callback;
35 | var uriBuilder = new UriBuilder(url);
36 | LNURL.AppendPayloadToQuery(uriBuilder, "k1", K1);
37 | LNURL.AppendPayloadToQuery(uriBuilder, "remoteid", ourId.ToString());
38 | LNURL.AppendPayloadToQuery(uriBuilder, "private",privateChannel? "1":"0");
39 |
40 | url = new Uri(uriBuilder.ToString());
41 | var response = await httpClient.GetAsync(url, cancellationToken);
42 | var json = JObject.Parse(await response.Content.ReadAsStringAsync(cancellationToken));
43 | if (LNUrlStatusResponse.IsErrorResponse(json, out var error)) throw new LNUrlException(error.Reason);
44 |
45 | }
46 |
47 | public async Task CancelRequest(PubKey ourId, HttpClient httpClient, CancellationToken cancellationToken = default)
48 | {
49 | var url = Callback;
50 | var uriBuilder = new UriBuilder(url);
51 | LNURL.AppendPayloadToQuery(uriBuilder, "k1", K1);
52 | LNURL.AppendPayloadToQuery(uriBuilder, "remoteid", ourId.ToString());
53 | LNURL.AppendPayloadToQuery(uriBuilder, "cancel", "1");
54 |
55 | url = new Uri(uriBuilder.ToString());
56 | var response = await httpClient.GetAsync(url, cancellationToken);
57 | var json = JObject.Parse(await response.Content.ReadAsStringAsync(cancellationToken));
58 | if (LNUrlStatusResponse.IsErrorResponse(json, out var error)) throw new LNUrlException(error.Reason);
59 | }
60 | }
--------------------------------------------------------------------------------
/LNURL/LNURLWithdrawRequest.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Net.Http;
3 | using System.Threading;
4 | using System.Threading.Tasks;
5 | using BTCPayServer.Lightning;
6 | using BTCPayServer.Lightning.JsonConverters;
7 | using LNURL.JsonConverters;
8 | using Newtonsoft.Json;
9 | using Newtonsoft.Json.Linq;
10 |
11 | namespace LNURL;
12 |
13 | ///
14 | /// https://github.com/fiatjaf/lnurl-rfc/blob/luds/02.md
15 | ///
16 | public class LNURLWithdrawRequest
17 | {
18 | [JsonProperty("callback")]
19 | [JsonConverter(typeof(UriJsonConverter))]
20 | public Uri Callback { get; set; }
21 |
22 | [JsonProperty("k1")] public string K1 { get; set; }
23 |
24 | [JsonProperty("tag")] public string Tag { get; set; }
25 |
26 | [JsonProperty("defaultDescription")] public string DefaultDescription { get; set; }
27 |
28 | [JsonProperty("minWithdrawable")]
29 | [JsonConverter(typeof(LightMoneyJsonConverter))]
30 | public LightMoney MinWithdrawable { get; set; }
31 |
32 | [JsonProperty("maxWithdrawable")]
33 | [JsonConverter(typeof(LightMoneyJsonConverter))]
34 | public LightMoney MaxWithdrawable { get; set; }
35 |
36 | //https://github.com/fiatjaf/lnurl-rfc/blob/luds/14.md
37 | [JsonProperty("currentBalance")]
38 | [JsonConverter(typeof(LightMoneyJsonConverter))]
39 | public LightMoney CurrentBalance { get; set; }
40 |
41 | //https://github.com/fiatjaf/lnurl-rfc/blob/luds/14.md
42 | [JsonProperty("balanceCheck")]
43 | [JsonConverter(typeof(UriJsonConverter))]
44 | public Uri BalanceCheck { get; set; }
45 |
46 | //https://github.com/fiatjaf/lnurl-rfc/blob/luds/19.md
47 | [JsonProperty("payLink", NullValueHandling = NullValueHandling.Ignore)]
48 | [JsonConverter(typeof(UriJsonConverter))]
49 | public Uri PayLink { get; set; }
50 |
51 | //https://github.com/bitcoin-ring/luds/blob/withdraw-pin/21.md
52 | [JsonProperty("pinLimit", NullValueHandling = NullValueHandling.Ignore)]
53 | [JsonConverter(typeof(LightMoneyJsonConverter))]
54 | public LightMoney PinLimit { get; set; }
55 |
56 | //https://github.com/fiatjaf/lnurl-rfc/blob/luds/15.md
57 | public Task SendRequest(string bolt11, HttpClient httpClient,
58 | Uri balanceNotify = null, CancellationToken cancellationToken = default)
59 | {
60 | return SendRequest(bolt11, httpClient, null, balanceNotify, cancellationToken);
61 | }
62 | public async Task SendRequest(string bolt11, HttpClient httpClient, string pin = null,
63 | Uri balanceNotify = null, CancellationToken cancellationToken = default)
64 | {
65 | var url = Callback;
66 | var uriBuilder = new UriBuilder(url);
67 | LNURL.AppendPayloadToQuery(uriBuilder, "pr", bolt11);
68 | LNURL.AppendPayloadToQuery(uriBuilder, "k1", K1);
69 | if (balanceNotify != null) LNURL.AppendPayloadToQuery(uriBuilder, "balanceNotify", balanceNotify.ToString());
70 | if (pin != null) LNURL.AppendPayloadToQuery(uriBuilder, "pin", pin);
71 |
72 | url = new Uri(uriBuilder.ToString());
73 | var response = await httpClient.GetAsync(url, cancellationToken);
74 | var json = JObject.Parse(await response.Content.ReadAsStringAsync(cancellationToken));
75 |
76 | return json.ToObject();
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/LNURL/LNAuthRequest.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Net.Http;
3 | using System.Threading;
4 | using System.Threading.Tasks;
5 | using NBitcoin;
6 | using NBitcoin.Crypto;
7 | using NBitcoin.DataEncoders;
8 | using Newtonsoft.Json;
9 | using Newtonsoft.Json.Converters;
10 | using Newtonsoft.Json.Linq;
11 |
12 | namespace LNURL;
13 |
14 | ///
15 | /// https://github.com/fiatjaf/lnurl-rfc/blob/luds/04.md
16 | ///
17 | public class LNAuthRequest
18 | {
19 | public enum LNAuthRequestAction
20 | {
21 | Register,
22 | Login,
23 | Link,
24 | Auth
25 | }
26 |
27 | public Uri LNUrl { get; set; }
28 |
29 |
30 | [JsonProperty("tag")] public string Tag => "login";
31 | [JsonProperty("k1")] public string K1 { get; set; }
32 |
33 | [JsonProperty("action")]
34 | [JsonConverter(typeof(StringEnumConverter))]
35 | public LNAuthRequestAction? Action { get; set; }
36 |
37 | public async Task SendChallenge(ECDSASignature sig, PubKey key, HttpClient httpClient, CancellationToken cancellationToken = default)
38 | {
39 | var url = LNUrl;
40 | var uriBuilder = new UriBuilder(url);
41 | LNURL.AppendPayloadToQuery(uriBuilder, "sig", Encoders.Hex.EncodeData(sig.ToDER()));
42 | LNURL.AppendPayloadToQuery(uriBuilder, "key", key.ToHex());
43 | url = new Uri(uriBuilder.ToString());
44 | var response = await httpClient.GetAsync(url, cancellationToken);
45 | var json = JObject.Parse(await response.Content.ReadAsStringAsync(cancellationToken));
46 |
47 | return json.ToObject();
48 | }
49 |
50 | public Task SendChallenge(Key key, HttpClient httpClient, CancellationToken cancellationToken = default)
51 | {
52 | var sig = SignChallenge(key);
53 | return SendChallenge(sig, key.PubKey, httpClient, cancellationToken);
54 | }
55 |
56 | public ECDSASignature SignChallenge(Key key)
57 | {
58 | return SignChallenge(key, K1);
59 | }
60 |
61 | public static ECDSASignature SignChallenge(Key key, string k1)
62 | {
63 | var messageBytes = Encoders.Hex.DecodeData(k1);
64 | var messageHash = new uint256(messageBytes);
65 | return key.Sign(messageHash);
66 | }
67 |
68 | public static void EnsureValidUrl(Uri serviceUrl)
69 | {
70 | var tag = serviceUrl.ParseQueryString().Get("tag");
71 | if (tag != "login")
72 | throw new ArgumentException(nameof(serviceUrl),
73 | "LNURL-Auth(LUD04) requires tag to be provided straight away");
74 | var k1 = serviceUrl.ParseQueryString().Get("k1");
75 | if (k1 is null) throw new ArgumentException(nameof(serviceUrl), "LNURL-Auth(LUD04) requires k1 to be provided");
76 |
77 | byte[] k1Bytes;
78 | try
79 | {
80 | k1Bytes = Encoders.Hex.DecodeData(k1);
81 | }
82 | catch (Exception)
83 | {
84 | throw new ArgumentException(nameof(serviceUrl), "LNURL-Auth(LUD04) requires k1 to be hex encoded");
85 | }
86 |
87 | if (k1Bytes.Length != 32)
88 | throw new ArgumentException(nameof(serviceUrl), "LNURL-Auth(LUD04) requires k1 to be 32bytes");
89 |
90 | var action = serviceUrl.ParseQueryString().Get("action");
91 | if (action != null && !Enum.TryParse(typeof(LNAuthRequestAction), action, true, out _))
92 | throw new ArgumentException(nameof(serviceUrl), "LNURL-Auth(LUD04) action value was invalid");
93 | }
94 |
95 | public static bool VerifyChallenge(ECDSASignature sig, PubKey expectedPubKey, byte[] expectedMessage)
96 | {
97 | var messageHash = new uint256(expectedMessage);
98 | return expectedPubKey.Verify(messageHash, sig);
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/LNURL/LNURL.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Collections.Specialized;
4 | using System.Linq;
5 | using System.Net;
6 | using System.Net.Http;
7 | using System.Text;
8 | using System.Threading;
9 | using System.Threading.Tasks;
10 | using Newtonsoft.Json.Linq;
11 |
12 | namespace LNURL;
13 |
14 | public class LNURL
15 | {
16 | private static readonly Dictionary SchemeTagMapping =
17 | new(StringComparer.InvariantCultureIgnoreCase)
18 | {
19 | {"lnurlc", "channelRequest"},
20 | {"lnurlw", "withdrawRequest"},
21 | {"lnurlp", "payRequest"},
22 | {"keyauth", "login"}
23 | };
24 |
25 | private static readonly Dictionary SchemeTagMappingReversed =
26 | SchemeTagMapping.ToDictionary(pair => pair.Value, pair => pair.Key,
27 | StringComparer.InvariantCultureIgnoreCase);
28 |
29 | public static void AppendPayloadToQuery(UriBuilder uri, string key, string value)
30 | {
31 | if (uri.Query.Length > 1)
32 | uri.Query += "&";
33 |
34 | uri.Query = uri.Query + WebUtility.UrlEncode(key) + "=" +
35 | WebUtility.UrlEncode(value);
36 | }
37 |
38 | public static Uri Parse(string lnurl, out string tag)
39 | {
40 | lnurl = lnurl.Replace("lightning:", "", StringComparison.InvariantCultureIgnoreCase);
41 | if (lnurl.StartsWith("lnurl1", StringComparison.InvariantCultureIgnoreCase))
42 | {
43 | Bech32Engine.Decode(lnurl, out _, out var data);
44 | var result = new Uri(Encoding.UTF8.GetString(data));
45 |
46 | if (!result.IsOnion() && !result.Scheme.Equals("https") && !result.IsLocalNetwork())
47 | throw new FormatException("LNURL provided is not secure.");
48 |
49 | var query = result.ParseQueryString();
50 | tag = query.Get("tag");
51 | return result;
52 | }
53 |
54 | if (Uri.TryCreate(lnurl, UriKind.Absolute, out var lud17Uri) &&
55 | SchemeTagMapping.TryGetValue(lud17Uri.Scheme.ToLowerInvariant(), out tag))
56 | return new Uri(lud17Uri.ToString()
57 | .Replace(lud17Uri.Scheme + ":", lud17Uri.IsOnion() ? "http:" : "https:"));
58 |
59 | throw new FormatException("LNURL uses bech32 and 'lnurl' as the hrp (LUD1) or an lnurl LUD17 scheme. ");
60 | }
61 |
62 | public static string EncodeBech32(Uri serviceUrl)
63 | {
64 | if (serviceUrl.Scheme != "https" && !serviceUrl.IsOnion() && !serviceUrl.IsLocalNetwork())
65 | throw new ArgumentException("serviceUrl must be an onion service OR https based OR on the local network",
66 | nameof(serviceUrl));
67 |
68 | return Bech32Engine.Encode("lnurl", Encoding.UTF8.GetBytes(serviceUrl.ToString()));
69 | }
70 |
71 | public static Uri EncodeUri(Uri serviceUrl, string tag, bool bech32)
72 | {
73 | if (serviceUrl.Scheme != "https" && !serviceUrl.IsOnion() && !serviceUrl.IsLocalNetwork())
74 | throw new ArgumentException("serviceUrl must be an onion service OR https based OR on the local network",
75 | nameof(serviceUrl));
76 | if (string.IsNullOrEmpty(tag)) tag = serviceUrl.ParseQueryString().Get("tag");
77 | if (tag == "login") LNAuthRequest.EnsureValidUrl(serviceUrl);
78 | if (bech32) return new Uri($"lightning:{EncodeBech32(serviceUrl)}");
79 |
80 | if (string.IsNullOrEmpty(tag)) tag = serviceUrl.ParseQueryString().Get("tag");
81 |
82 | if (string.IsNullOrEmpty(tag)) throw new ArgumentNullException("tag must be provided", nameof(tag));
83 |
84 | if (!SchemeTagMappingReversed.TryGetValue(tag.ToLowerInvariant(), out var scheme))
85 | throw new ArgumentOutOfRangeException(
86 | $"tag must be either {string.Join(',', SchemeTagMappingReversed.Select(pair => pair.Key))}",
87 | nameof(tag));
88 |
89 |
90 | return new UriBuilder(serviceUrl)
91 | {
92 | Scheme = scheme
93 | }.Uri;
94 | }
95 |
96 | //https://github.com/fiatjaf/lnurl-rfc/blob/luds/16.md
97 | public static Task FetchPayRequestViaInternetIdentifier(string identifier,
98 | HttpClient httpClient)
99 | {
100 | return FetchPayRequestViaInternetIdentifier(identifier, httpClient, default);
101 | }
102 | public static async Task FetchPayRequestViaInternetIdentifier(string identifier,
103 | HttpClient httpClient, CancellationToken cancellationToken)
104 | {
105 | return (LNURLPayRequest) await FetchInformation(ExtractUriFromInternetIdentifier(identifier), "payRequest",
106 | httpClient, cancellationToken);
107 | }
108 |
109 | public static Uri ExtractUriFromInternetIdentifier(string identifier)
110 | {
111 | var s = identifier.Split("@");
112 | var s2 = s[1].Split(":");
113 | UriBuilder uriBuilder;
114 | if (s2.Length > 1)
115 | uriBuilder = new UriBuilder(
116 | s2[0].EndsWith(".onion", StringComparison.InvariantCultureIgnoreCase) ? "http" : "https",
117 | s2[0], int.Parse(s2[1]))
118 | {
119 | Path = $"/.well-known/lnurlp/{s[0]}"
120 | };
121 | else
122 | uriBuilder =
123 | new UriBuilder(s[1].EndsWith(".onion", StringComparison.InvariantCultureIgnoreCase) ? "http" : "https",
124 | s2[0])
125 | {
126 | Path = $"/.well-known/lnurlp/{s[0]}"
127 | };
128 |
129 | return uriBuilder.Uri;
130 | }
131 |
132 |
133 | public static Task