mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-18 14:34:23 +01:00
Boltcard integration (#5419)
* Boltcard integration * Add API for boltcard registration
This commit is contained in:
@@ -20,6 +20,12 @@ namespace BTCPayServer.Client
|
||||
return await HandleResponse<PullPaymentData>(response);
|
||||
}
|
||||
|
||||
public virtual async Task<RegisterBoltcardResponse> RegisterBoltcard(string pullPaymentId, RegisterBoltcardRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/pull-payments/{HttpUtility.UrlEncode(pullPaymentId)}/boltcards", bodyPayload: request, method: HttpMethod.Post), cancellationToken);
|
||||
return await HandleResponse<RegisterBoltcardResponse>(response);
|
||||
}
|
||||
|
||||
public virtual async Task<PullPaymentData[]> GetPullPayments(string storeId, bool includeArchived = false, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Dictionary<string, object> query = new Dictionary<string, object>();
|
||||
|
||||
39
BTCPayServer.Client/Models/RegisterBoltcardRequest.cs
Normal file
39
BTCPayServer.Client/Models/RegisterBoltcardRequest.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using NBitcoin.JsonConverters;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public enum OnExistingBehavior
|
||||
{
|
||||
KeepVersion,
|
||||
UpdateVersion
|
||||
}
|
||||
public class RegisterBoltcardRequest
|
||||
{
|
||||
[JsonConverter(typeof(HexJsonConverter))]
|
||||
[JsonProperty("UID")]
|
||||
public byte[] UID { get; set; }
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public OnExistingBehavior? OnExisting { get; set; }
|
||||
}
|
||||
public class RegisterBoltcardResponse
|
||||
{
|
||||
[JsonProperty("LNURLW")]
|
||||
public string LNURLW { get; set; }
|
||||
public int Version { get; set; }
|
||||
[JsonProperty("K0")]
|
||||
public string K0 { get; set; }
|
||||
[JsonProperty("K1")]
|
||||
public string K1 { get; set; }
|
||||
[JsonProperty("K2")]
|
||||
public string K2 { get; set; }
|
||||
[JsonProperty("K3")]
|
||||
public string K3 { get; set; }
|
||||
[JsonProperty("K4")]
|
||||
public string K4 { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
using System.Security.Permissions;
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Internal;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20231020135844_AddBoltcardsTable")]
|
||||
public partial class AddBoltcardsTable : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "boltcards",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<string>(maxLength: 32, nullable: false),
|
||||
counter = table.Column<int>(type: "INT", nullable: false, defaultValue: 0),
|
||||
ppid = table.Column<string>(maxLength: 30, nullable: true),
|
||||
version = table.Column<int>(nullable: false, defaultValue: 0)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_id", x => x.id);
|
||||
table.ForeignKey("FK_boltcards_PullPayments", x => x.ppid, "PullPayments", "Id", onDelete: ReferentialAction.SetNull);
|
||||
});
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable("boltcards");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -197,10 +197,11 @@ retry:
|
||||
driver.FindElement(selector).Click();
|
||||
}
|
||||
|
||||
[DebuggerHidden]
|
||||
public static bool ElementDoesNotExist(this IWebDriver driver, By selector)
|
||||
{
|
||||
Assert.Throws<NoSuchElementException>(() =>
|
||||
Assert.Throws<NoSuchElementException>(
|
||||
[DebuggerStepThrough]
|
||||
() =>
|
||||
{
|
||||
driver.FindElement(selector);
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
@@ -1085,6 +1086,38 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal(12.303228134m, test4.Amount);
|
||||
Assert.Equal("BTC", test4.Currency);
|
||||
|
||||
// Check we can register Boltcard
|
||||
var uid = new byte[7];
|
||||
RandomNumberGenerator.Fill(uid);
|
||||
var card = await client.RegisterBoltcard(test4.Id, new RegisterBoltcardRequest()
|
||||
{
|
||||
UID = uid
|
||||
});
|
||||
Assert.Equal(0, card.Version);
|
||||
var card1keys = new[] { card.K0, card.K1, card.K2, card.K3, card.K4 };
|
||||
Assert.DoesNotContain(null, card1keys);
|
||||
var card2 = await client.RegisterBoltcard(test4.Id, new RegisterBoltcardRequest()
|
||||
{
|
||||
UID = uid
|
||||
});
|
||||
Assert.Equal(1, card2.Version);
|
||||
Assert.StartsWith("lnurlw://", card2.LNURLW);
|
||||
Assert.EndsWith("/boltcard", card2.LNURLW);
|
||||
var card2keys = new[] { card2.K0, card2.K1, card2.K2, card2.K3, card2.K4 };
|
||||
Assert.DoesNotContain(null, card2keys);
|
||||
for (int i = 0; i < card1keys.Length; i++)
|
||||
{
|
||||
if (i == 1)
|
||||
Assert.Contains(card1keys[i], card2keys);
|
||||
else
|
||||
Assert.DoesNotContain(card1keys[i], card2keys);
|
||||
}
|
||||
var card3 = await client.RegisterBoltcard(test4.Id, new RegisterBoltcardRequest()
|
||||
{
|
||||
UID = uid,
|
||||
OnExisting = OnExistingBehavior.KeepVersion
|
||||
});
|
||||
Assert.Equal(card2.Version, card3.Version);
|
||||
// Test with SATS denomination values
|
||||
var testSats = await client.CreatePullPayment(storeId, new Client.Models.CreatePullPaymentRequest()
|
||||
{
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
@@ -17,6 +18,7 @@ using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Models.InvoicingModels;
|
||||
using BTCPayServer.NTag424;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
@@ -25,6 +27,8 @@ using BTCPayServer.Views.Manage;
|
||||
using BTCPayServer.Views.Server;
|
||||
using BTCPayServer.Views.Stores;
|
||||
using BTCPayServer.Views.Wallets;
|
||||
using Dapper;
|
||||
using ExchangeSharp;
|
||||
using LNURL;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@@ -2116,6 +2120,7 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.FindElement(By.CssSelector("#lnurlwithdraw-button")).Click();
|
||||
s.Driver.WaitForElement(By.Id("qr-code-data-input"));
|
||||
|
||||
// Try to use lnurlw via the QR Code
|
||||
var lnurl = new Uri(LNURL.LNURL.Parse(s.Driver.FindElement(By.Id("qr-code-data-input")).GetAttribute("value"), out _).ToString().Replace("https", "http"));
|
||||
s.Driver.FindElement(By.CssSelector("button[data-bs-dismiss='modal']")).Click();
|
||||
var info = Assert.IsType<LNURLWithdrawRequest>(await LNURL.LNURL.FetchInformation(lnurl, s.Server.PayTester.HttpClient));
|
||||
@@ -2126,7 +2131,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal(info.CurrentBalance, new LightMoney(0.0000001m, LightMoneyUnit.BTC));
|
||||
|
||||
var bolt2 = (await s.Server.CustomerLightningD.CreateInvoice(
|
||||
new LightMoney(0.0000001m, LightMoneyUnit.BTC),
|
||||
new LightMoney(0.00000005m, LightMoneyUnit.BTC),
|
||||
$"LNurl w payout test {DateTime.UtcNow.Ticks}",
|
||||
TimeSpan.FromHours(1), CancellationToken.None));
|
||||
var response = await info.SendRequest(bolt2.BOLT11, s.Server.PayTester.HttpClient, null,null);
|
||||
@@ -2141,6 +2146,52 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.Close();
|
||||
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
|
||||
|
||||
// Simulate a boltcard
|
||||
{
|
||||
var db = s.Server.PayTester.GetService<ApplicationDbContextFactory>();
|
||||
var ppid = lnurl.AbsoluteUri.Split("/").Last();
|
||||
var issuerKey = new IssuerKey(SettingsRepositoryExtensions.FixedKey());
|
||||
var uid = RandomNumberGenerator.GetBytes(7);
|
||||
var cardKey = issuerKey.CreateCardKey(uid, 0);
|
||||
var keys = cardKey.DeriveBoltcardKeys(issuerKey);
|
||||
await db.LinkBoltcardToPullPayment(ppid, issuerKey, uid);
|
||||
var piccData = new byte[] { 0xc7 }.Concat(uid).Concat(new byte[] { 1, 0, 0, 0, 0, 0, 0, 0 }).ToArray();
|
||||
var p = keys.EncryptionKey.Encrypt(piccData);
|
||||
var c = keys.AuthenticationKey.GetSunMac(uid, 1);
|
||||
var boltcardUrl = new Uri(s.Server.PayTester.ServerUri.AbsoluteUri + $"boltcard?p={Encoders.Hex.EncodeData(p).ToStringUpperInvariant()}&c={Encoders.Hex.EncodeData(c).ToStringUpperInvariant()}");
|
||||
// p and c should work so long as no bolt11 has been submitted
|
||||
info = (LNURLWithdrawRequest)await LNURL.LNURL.FetchInformation(boltcardUrl, s.Server.PayTester.HttpClient);
|
||||
info = (LNURLWithdrawRequest)await LNURL.LNURL.FetchInformation(boltcardUrl, s.Server.PayTester.HttpClient);
|
||||
var fakeBoltcardUrl = new Uri(Regex.Replace(boltcardUrl.AbsoluteUri, "p=([A-F0-9]{32})", $"p={RandomBytes(16)}"));
|
||||
await Assert.ThrowsAsync<LNUrlException>(() => LNURL.LNURL.FetchInformation(fakeBoltcardUrl, s.Server.PayTester.HttpClient));
|
||||
fakeBoltcardUrl = new Uri(Regex.Replace(boltcardUrl.AbsoluteUri, "c=([A-F0-9]{16})", $"c={RandomBytes(8)}"));
|
||||
await Assert.ThrowsAsync<LNUrlException>(() => LNURL.LNURL.FetchInformation(fakeBoltcardUrl, s.Server.PayTester.HttpClient));
|
||||
|
||||
bolt2 = (await s.Server.CustomerLightningD.CreateInvoice(
|
||||
new LightMoney(0.00000005m, LightMoneyUnit.BTC),
|
||||
$"LNurl w payout test2 {DateTime.UtcNow.Ticks}",
|
||||
TimeSpan.FromHours(1), CancellationToken.None));
|
||||
response = await info.SendRequest(bolt2.BOLT11, s.Server.PayTester.HttpClient, null, null);
|
||||
Assert.Equal("OK", response.Status);
|
||||
// No replay should be possible
|
||||
await Assert.ThrowsAsync<LNUrlException>(() => LNURL.LNURL.FetchInformation(boltcardUrl, s.Server.PayTester.HttpClient));
|
||||
response = await info.SendRequest(bolt2.BOLT11, s.Server.PayTester.HttpClient, null, null);
|
||||
Assert.Equal("ERROR", response.Status);
|
||||
Assert.Contains("Replayed", response.Reason);
|
||||
|
||||
// Check the state of the registration, counter should have increased
|
||||
var reg = await db.GetBoltcardRegistration(issuerKey, uid);
|
||||
Assert.Equal((ppid, 1, 0), (reg.PullPaymentId, reg.Counter, reg.Version));
|
||||
await db.SetBoltcardResetState(issuerKey, uid);
|
||||
// After reset, counter is 0, version unchanged and ppId null
|
||||
reg = await db.GetBoltcardRegistration(issuerKey, uid);
|
||||
Assert.Equal((null, 0, 0), (reg.PullPaymentId, reg.Counter, reg.Version));
|
||||
await db.LinkBoltcardToPullPayment(ppid, issuerKey, uid);
|
||||
// Relink should bump Version
|
||||
reg = await db.GetBoltcardRegistration(issuerKey, uid);
|
||||
Assert.Equal((ppid, 0, 1), (reg.PullPaymentId, reg.Counter, reg.Version));
|
||||
}
|
||||
|
||||
s.GoToStore(s.StoreId, StoreNavPages.PullPayments);
|
||||
s.Driver.FindElement(By.Id("NewPullPayment")).Click();
|
||||
s.Driver.FindElement(By.Id("Name")).SendKeys("PP1");
|
||||
@@ -2221,6 +2272,12 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.Close();
|
||||
}
|
||||
|
||||
private string RandomBytes(int count)
|
||||
{
|
||||
var c = RandomNumberGenerator.GetBytes(count);
|
||||
return Encoders.Hex.EncodeData(c);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Selenium", "Selenium")]
|
||||
[Trait("Lightning", "Lightning")]
|
||||
|
||||
42
BTCPayServer/APDUVaultTransport.cs
Normal file
42
BTCPayServer/APDUVaultTransport.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Net.WebSockets;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
using BTCPayServer.NTag424;
|
||||
using NBitcoin.DataEncoders;
|
||||
using System;
|
||||
using SocketIOClient;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public class APDUVaultTransport : IAPDUTransport
|
||||
{
|
||||
private readonly VaultClient _vaultClient;
|
||||
|
||||
public APDUVaultTransport(VaultClient vaultClient)
|
||||
{
|
||||
_vaultClient = vaultClient;
|
||||
}
|
||||
|
||||
|
||||
public async Task WaitForCard(CancellationToken cancellationToken)
|
||||
{
|
||||
await _vaultClient.SendVaultRequest("/wait-for-card", null, cancellationToken);
|
||||
}
|
||||
public async Task WaitForRemoved(CancellationToken cancellationToken)
|
||||
{
|
||||
await _vaultClient.SendVaultRequest("/wait-for-disconnected", null, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<NtagResponse> SendAPDU(byte[] apdu, CancellationToken cancellationToken)
|
||||
{
|
||||
var resp = await _vaultClient.SendVaultRequest("/",
|
||||
new JObject()
|
||||
{
|
||||
["apdu"] = Encoders.Hex.EncodeData(apdu)
|
||||
}, cancellationToken);
|
||||
var data = Encoders.Hex.DecodeData(resp["data"].Value<string>());
|
||||
return new NtagResponse(data, resp["status"].Value<ushort>());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,6 +46,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BTCPayServer.NTag424" Version="1.0.17" />
|
||||
<PackageReference Include="YamlDotNet" Version="8.0.0" />
|
||||
<PackageReference Include="BIP78.Sender" Version="0.2.2" />
|
||||
<PackageReference Include="BTCPayServer.Hwi" Version="2.0.2" />
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
@@ -10,6 +11,7 @@ using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.NTag424;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services;
|
||||
@@ -19,6 +21,7 @@ using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NBitcoin.DataEncoders;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using MarkPayoutRequest = BTCPayServer.HostedServices.MarkPayoutRequest;
|
||||
|
||||
@@ -37,6 +40,8 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
private readonly IEnumerable<IPayoutHandler> _payoutHandlers;
|
||||
private readonly BTCPayNetworkProvider _networkProvider;
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly SettingsRepository _settingsRepository;
|
||||
private readonly BTCPayServerEnvironment _env;
|
||||
|
||||
public GreenfieldPullPaymentController(PullPaymentHostedService pullPaymentService,
|
||||
LinkGenerator linkGenerator,
|
||||
@@ -45,7 +50,9 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
Services.BTCPayNetworkJsonSerializerSettings serializerSettings,
|
||||
IEnumerable<IPayoutHandler> payoutHandlers,
|
||||
BTCPayNetworkProvider btcPayNetworkProvider,
|
||||
IAuthorizationService authorizationService)
|
||||
IAuthorizationService authorizationService,
|
||||
SettingsRepository settingsRepository,
|
||||
BTCPayServerEnvironment env)
|
||||
{
|
||||
_pullPaymentService = pullPaymentService;
|
||||
_linkGenerator = linkGenerator;
|
||||
@@ -55,6 +62,8 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
_payoutHandlers = payoutHandlers;
|
||||
_networkProvider = btcPayNetworkProvider;
|
||||
_authorizationService = authorizationService;
|
||||
_settingsRepository = settingsRepository;
|
||||
_env = env;
|
||||
}
|
||||
|
||||
[HttpGet("~/api/v1/stores/{storeId}/pull-payments")]
|
||||
@@ -187,6 +196,46 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
};
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("~/api/v1/pull-payments/{pullPaymentId}/boltcards")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> RegisterBoltcard(string pullPaymentId, RegisterBoltcardRequest request)
|
||||
{
|
||||
if (pullPaymentId is null)
|
||||
return PullPaymentNotFound();
|
||||
var pp = await _pullPaymentService.GetPullPayment(pullPaymentId, false);
|
||||
if (pp is null)
|
||||
return PullPaymentNotFound();
|
||||
if (request?.UID is null || request.UID.Length != 7)
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.UID), "The UID is required and should be 7 bytes");
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
if (!_pullPaymentService.SupportsLNURL(pp.GetBlob()))
|
||||
{
|
||||
return this.CreateAPIError(400, "lnurl-not-supported", "This pull payment currency should be BTC or SATS and accept lightning");
|
||||
}
|
||||
|
||||
var issuerKey = await _settingsRepository.GetIssuerKey(_env);
|
||||
var version = await _dbContextFactory.LinkBoltcardToPullPayment(pullPaymentId, issuerKey, request.UID, request.OnExisting);
|
||||
var keys = issuerKey.CreateCardKey(request.UID, version).DeriveBoltcardKeys(issuerKey);
|
||||
|
||||
var boltcardUrl = Url.Action(nameof(UIBoltcardController.GetWithdrawRequest), "UIBoltcard");
|
||||
boltcardUrl = Request.GetAbsoluteUri(boltcardUrl);
|
||||
boltcardUrl = Regex.Replace(boltcardUrl, "^https?://", "lnurlw://");
|
||||
|
||||
return Ok(new RegisterBoltcardResponse()
|
||||
{
|
||||
LNURLW = boltcardUrl,
|
||||
Version = version,
|
||||
K0 = Encoders.Hex.EncodeData(keys.AppMasterKey.ToBytes()).ToUpperInvariant(),
|
||||
K1 = Encoders.Hex.EncodeData(keys.EncryptionKey.ToBytes()).ToUpperInvariant(),
|
||||
K2 = Encoders.Hex.EncodeData(keys.AuthenticationKey.ToBytes()).ToUpperInvariant(),
|
||||
K3 = Encoders.Hex.EncodeData(keys.K3.ToBytes()).ToUpperInvariant(),
|
||||
K4 = Encoders.Hex.EncodeData(keys.K4.ToBytes()).ToUpperInvariant(),
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("~/api/v1/pull-payments/{pullPaymentId}")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> GetPullPayment(string pullPaymentId)
|
||||
|
||||
@@ -1275,6 +1275,11 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
return GetFromActionResult<PayoutData>(await GetController<GreenfieldPullPaymentController>().GetPayout(pullPaymentId, payoutId));
|
||||
}
|
||||
|
||||
public override async Task<RegisterBoltcardResponse> RegisterBoltcard(string pullPaymentId, RegisterBoltcardRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return GetFromActionResult<RegisterBoltcardResponse>(await GetController<GreenfieldPullPaymentController>().RegisterBoltcard(pullPaymentId, request));
|
||||
}
|
||||
|
||||
public override async Task<PullPaymentLNURL> GetPullPaymentLNURL(string pullPaymentId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return GetFromActionResult<PullPaymentLNURL>(await GetController<GreenfieldPullPaymentController>().GetPullPaymentLNURL(pullPaymentId));
|
||||
|
||||
70
BTCPayServer/Controllers/UIBoltcardController.cs
Normal file
70
BTCPayServer/Controllers/UIBoltcardController.cs
Normal file
@@ -0,0 +1,70 @@
|
||||
#nullable enable
|
||||
using Dapper;
|
||||
using System.Linq;
|
||||
using System.Security;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.NTag424;
|
||||
using BTCPayServer.Services;
|
||||
using LNURL;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Threading;
|
||||
using System;
|
||||
using NBitcoin.DataEncoders;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace BTCPayServer.Controllers;
|
||||
|
||||
public class UIBoltcardController : Controller
|
||||
{
|
||||
public class BoltcardSettings
|
||||
{
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.HexJsonConverter))]
|
||||
public byte[]? IssuerKey { get; set; }
|
||||
}
|
||||
public UIBoltcardController(
|
||||
UILNURLController lnUrlController,
|
||||
SettingsRepository settingsRepository,
|
||||
ApplicationDbContextFactory contextFactory,
|
||||
BTCPayServerEnvironment env)
|
||||
{
|
||||
LNURLController = lnUrlController;
|
||||
SettingsRepository = settingsRepository;
|
||||
ContextFactory = contextFactory;
|
||||
Env = env;
|
||||
}
|
||||
|
||||
public UILNURLController LNURLController { get; }
|
||||
public SettingsRepository SettingsRepository { get; }
|
||||
public ApplicationDbContextFactory ContextFactory { get; }
|
||||
public BTCPayServerEnvironment Env { get; }
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpGet("~/boltcard")]
|
||||
public async Task<IActionResult> GetWithdrawRequest([FromQuery] string? p, [FromQuery] string? c, [FromQuery] string? pr, [FromQuery] string? k1, CancellationToken cancellationToken)
|
||||
{
|
||||
if (p is null || c is null)
|
||||
{
|
||||
var k1s = k1?.Split('-');
|
||||
if (k1s is not { Length: 2 })
|
||||
return BadRequest(new LNUrlStatusResponse { Status = "ERROR", Reason = "Missing p, c, or k1 query parameter" });
|
||||
p = k1s[0];
|
||||
c = k1s[1];
|
||||
}
|
||||
var issuerKey = await SettingsRepository.GetIssuerKey(Env);
|
||||
var piccData = issuerKey.TryDecrypt(p);
|
||||
if (piccData is null)
|
||||
return BadRequest(new LNUrlStatusResponse { Status = "ERROR", Reason = "Invalid PICCData" });
|
||||
|
||||
var registration = await ContextFactory.GetBoltcardRegistration(issuerKey, piccData, updateCounter: pr is not null);
|
||||
if (registration?.PullPaymentId is null)
|
||||
return BadRequest(new LNUrlStatusResponse { Status = "ERROR", Reason = "Replayed or expired query" });
|
||||
var cardKey = issuerKey.CreateCardKey(piccData.Uid, registration.Version);
|
||||
if (!cardKey.CheckSunMac(c, piccData))
|
||||
return BadRequest(new LNUrlStatusResponse { Status = "ERROR", Reason = "Replayed or expired query" });
|
||||
LNURLController.ControllerContext.HttpContext = HttpContext;
|
||||
return await LNURLController.GetLNURLForPullPayment("BTC", registration.PullPaymentId, pr, $"{p}-{c}", cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -90,9 +90,13 @@ namespace BTCPayServer
|
||||
_pluginHookService = pluginHookService;
|
||||
_invoiceActivator = invoiceActivator;
|
||||
}
|
||||
|
||||
[HttpGet("withdraw/pp/{pullPaymentId}")]
|
||||
public async Task<IActionResult> GetLNURLForPullPayment(string cryptoCode, string pullPaymentId, [FromQuery] string pr, CancellationToken cancellationToken)
|
||||
public Task<IActionResult> GetLNURLForPullPayment(string cryptoCode, string pullPaymentId, [FromQuery] string pr, CancellationToken cancellationToken)
|
||||
{
|
||||
return GetLNURLForPullPayment(cryptoCode, pullPaymentId, pr, pullPaymentId, cancellationToken);
|
||||
}
|
||||
[NonAction]
|
||||
internal async Task<IActionResult> GetLNURLForPullPayment(string cryptoCode, string pullPaymentId, string pr, string k1, CancellationToken cancellationToken)
|
||||
{
|
||||
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
|
||||
if (network is null || !network.SupportLightning)
|
||||
@@ -119,7 +123,7 @@ namespace BTCPayServer
|
||||
var request = new LNURLWithdrawRequest
|
||||
{
|
||||
MaxWithdrawable = LightMoney.FromUnit(remaining, unit),
|
||||
K1 = pullPaymentId,
|
||||
K1 = k1,
|
||||
BalanceCheck = new Uri(Request.GetCurrentUrl()),
|
||||
CurrentBalance = LightMoney.FromUnit(remaining, unit),
|
||||
MinWithdrawable =
|
||||
|
||||
202
BTCPayServer/Controllers/UIPullPaymentController.Boltcard.cs
Normal file
202
BTCPayServer/Controllers/UIPullPaymentController.Boltcard.cs
Normal file
@@ -0,0 +1,202 @@
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.NTag424;
|
||||
using Dapper;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NBitcoin.DataEncoders;
|
||||
using System;
|
||||
using System.Net.WebSockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using static BTCPayServer.BoltcardDataExtensions;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class UIPullPaymentController
|
||||
{
|
||||
[AllowAnonymous]
|
||||
[HttpGet("pull-payments/{pullPaymentId}/boltcard/{command}")]
|
||||
public IActionResult SetupBoltcard(string pullPaymentId, string command)
|
||||
{
|
||||
return View(nameof(SetupBoltcard), new SetupBoltcardViewModel()
|
||||
{
|
||||
ReturnUrl = Url.Action(nameof(ViewPullPayment), "UIPullPayment", new { pullPaymentId }),
|
||||
WebsocketPath = Url.Action(nameof(VaultNFCBridgeConnection), "UIPullPayment", new { pullPaymentId }),
|
||||
Command = command
|
||||
});
|
||||
}
|
||||
[AllowAnonymous]
|
||||
[HttpPost("pull-payments/{pullPaymentId}/boltcard/{command}")]
|
||||
public IActionResult SetupBoltcardPost(string pullPaymentId, string command)
|
||||
{
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Boltcard is configured";
|
||||
return RedirectToAction(nameof(ViewPullPayment), new { pullPaymentId });
|
||||
}
|
||||
|
||||
record CardOrigin
|
||||
{
|
||||
public record Blank() : CardOrigin;
|
||||
public record ThisIssuer(BoltcardRegistration Registration) : CardOrigin;
|
||||
public record ThisIssuerConfigured(string PullPaymentId, BoltcardRegistration Registration) : ThisIssuer(Registration);
|
||||
public record OtherIssuer() : CardOrigin;
|
||||
public record ThisIssuerReset(BoltcardRegistration Registration) : ThisIssuer(Registration);
|
||||
}
|
||||
[HttpGet]
|
||||
[Route("pull-payments/{pullPaymentId}/nfc/bridge")]
|
||||
public async Task<IActionResult> VaultNFCBridgeConnection(string pullPaymentId)
|
||||
{
|
||||
if (!HttpContext.WebSockets.IsWebSocketRequest)
|
||||
return NotFound();
|
||||
var pp = await _pullPaymentHostedService.GetPullPayment(pullPaymentId, false);
|
||||
if (pp is null)
|
||||
return NotFound();
|
||||
if (!_pullPaymentHostedService.SupportsLNURL(pp.GetBlob()))
|
||||
return BadRequest();
|
||||
|
||||
var boltcardUrl = Url.Action(nameof(UIBoltcardController.GetWithdrawRequest), "UIBoltcard");
|
||||
boltcardUrl = Request.GetAbsoluteUri(boltcardUrl);
|
||||
var websocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
|
||||
var vaultClient = new VaultClient(websocket);
|
||||
var transport = new APDUVaultTransport(vaultClient);
|
||||
var ntag = new Ntag424(transport);
|
||||
using (var cts = new CancellationTokenSource(TimeSpan.FromMinutes(10)))
|
||||
{
|
||||
next:
|
||||
while (websocket.State == System.Net.WebSockets.WebSocketState.Open)
|
||||
{
|
||||
try
|
||||
{
|
||||
var command = await vaultClient.GetNextCommand(cts.Token);
|
||||
var permission = await vaultClient.AskPermission(VaultServices.NFC, cts.Token);
|
||||
if (permission is null)
|
||||
{
|
||||
await vaultClient.Show(VaultMessageType.Error, "BTCPay Server Vault does not seem to be running, you can download it on <a target=\"_blank\" href=\"https://github.com/btcpayserver/BTCPayServer.Vault/releases/latest\">Github</a>.", cts.Token);
|
||||
goto next;
|
||||
}
|
||||
await vaultClient.Show(VaultMessageType.Ok, "BTCPayServer successfully connected to the vault.", cts.Token);
|
||||
if (permission is false)
|
||||
{
|
||||
await vaultClient.Show(VaultMessageType.Error, "The user declined access to the vault.", cts.Token);
|
||||
goto next;
|
||||
}
|
||||
await vaultClient.Show(VaultMessageType.Ok, "Access to vault granted by owner.", cts.Token);
|
||||
|
||||
await vaultClient.Show(VaultMessageType.Processing, "Waiting for NFC to be presented...", cts.Token);
|
||||
await transport.WaitForCard(cts.Token);
|
||||
await vaultClient.Show(VaultMessageType.Ok, "NFC detected.", cts.Token);
|
||||
|
||||
var issuerKey = await _settingsRepository.GetIssuerKey(_env);
|
||||
CardOrigin cardOrigin = await GetCardOrigin(pullPaymentId, ntag, issuerKey, cts.Token);
|
||||
|
||||
if (cardOrigin is CardOrigin.OtherIssuer)
|
||||
{
|
||||
await vaultClient.Show(VaultMessageType.Error, "This card is already configured for another issuer", cts.Token);
|
||||
goto next;
|
||||
}
|
||||
|
||||
bool success = false;
|
||||
switch (command)
|
||||
{
|
||||
case "configure-boltcard":
|
||||
await vaultClient.Show(VaultMessageType.Processing, "Configuring Boltcard...", cts.Token);
|
||||
if (cardOrigin is CardOrigin.Blank || cardOrigin is CardOrigin.ThisIssuerReset)
|
||||
{
|
||||
await ntag.AuthenticateEV2First(0, AESKey.Default, cts.Token);
|
||||
var uid = await ntag.GetCardUID();
|
||||
try
|
||||
{
|
||||
var version = await _dbContextFactory.LinkBoltcardToPullPayment(pullPaymentId, issuerKey, uid);
|
||||
var cardKey = issuerKey.CreateCardKey(uid, version);
|
||||
await ntag.SetupBoltcard(boltcardUrl, BoltcardKeys.Default, cardKey.DeriveBoltcardKeys(issuerKey));
|
||||
}
|
||||
catch
|
||||
{
|
||||
await _dbContextFactory.SetBoltcardResetState(issuerKey, uid);
|
||||
throw;
|
||||
}
|
||||
await vaultClient.Show(VaultMessageType.Ok, "The card is now configured", cts.Token);
|
||||
}
|
||||
else if (cardOrigin is CardOrigin.ThisIssuer)
|
||||
{
|
||||
await vaultClient.Show(VaultMessageType.Ok, "This card is already properly configured", cts.Token);
|
||||
}
|
||||
success = true;
|
||||
break;
|
||||
case "reset-boltcard":
|
||||
await vaultClient.Show(VaultMessageType.Processing, "Resetting Boltcard...", cts.Token);
|
||||
if (cardOrigin is CardOrigin.Blank)
|
||||
{
|
||||
await vaultClient.Show(VaultMessageType.Ok, "This card is already in a factory state", cts.Token);
|
||||
}
|
||||
else if (cardOrigin is CardOrigin.ThisIssuer thisIssuer)
|
||||
{
|
||||
var cardKey = issuerKey.CreateCardKey(thisIssuer.Registration.UId, thisIssuer.Registration.Version);
|
||||
await ntag.ResetCard(issuerKey, cardKey);
|
||||
await _dbContextFactory.SetBoltcardResetState(issuerKey, thisIssuer.Registration.UId);
|
||||
await vaultClient.Show(VaultMessageType.Ok, "Card reset succeed", cts.Token);
|
||||
}
|
||||
success = true;
|
||||
break;
|
||||
}
|
||||
if (success)
|
||||
{
|
||||
await vaultClient.Show(VaultMessageType.Processing, "Please remove the NFC from the card reader", cts.Token);
|
||||
await transport.WaitForRemoved(cts.Token);
|
||||
await vaultClient.Show(VaultMessageType.Ok, "Thank you!", cts.Token);
|
||||
await vaultClient.SendSimpleMessage("done", cts.Token);
|
||||
}
|
||||
}
|
||||
catch (Exception) when (websocket.State != WebSocketState.Open || cts.IsCancellationRequested)
|
||||
{
|
||||
await WebsocketHelper.CloseSocket(websocket);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
try
|
||||
{
|
||||
await vaultClient.Show(VaultMessageType.Error, "Unexpected error: " + ex.Message, ex.ToString(), cts.Token);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
return new EmptyResult();
|
||||
}
|
||||
|
||||
private async Task<CardOrigin> GetCardOrigin(string pullPaymentId, Ntag424 ntag, IssuerKey issuerKey, CancellationToken cancellationToken)
|
||||
{
|
||||
CardOrigin cardOrigin;
|
||||
Uri uri = await ntag.TryReadNDefURI(cancellationToken);
|
||||
|
||||
if (uri is null)
|
||||
{
|
||||
cardOrigin = new CardOrigin.Blank();
|
||||
}
|
||||
else
|
||||
{
|
||||
var piccData = issuerKey.TryDecrypt(uri);
|
||||
if (piccData is null)
|
||||
{
|
||||
cardOrigin = new CardOrigin.OtherIssuer();
|
||||
}
|
||||
else
|
||||
{
|
||||
var res = await _dbContextFactory.GetBoltcardRegistration(issuerKey, piccData.Uid);
|
||||
if (res != null && res.PullPaymentId is null)
|
||||
cardOrigin = new CardOrigin.ThisIssuerReset(res);
|
||||
else if (res?.PullPaymentId != pullPaymentId)
|
||||
cardOrigin = new CardOrigin.OtherIssuer();
|
||||
else
|
||||
cardOrigin = new CardOrigin.ThisIssuerConfigured(res.PullPaymentId, res);
|
||||
}
|
||||
}
|
||||
|
||||
return cardOrigin;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,11 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Amazon.S3.Model;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
@@ -10,20 +13,27 @@ using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.ModelBinders;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.WalletViewModels;
|
||||
using BTCPayServer.NTag424;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Dapper;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using NdefLibrary.Ndef;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public class UIPullPaymentController : Controller
|
||||
public partial class UIPullPaymentController : Controller
|
||||
{
|
||||
private readonly ApplicationDbContextFactory _dbContextFactory;
|
||||
private readonly CurrencyNameTable _currencyNameTable;
|
||||
@@ -33,6 +43,8 @@ namespace BTCPayServer.Controllers
|
||||
private readonly BTCPayNetworkJsonSerializerSettings _serializerSettings;
|
||||
private readonly IEnumerable<IPayoutHandler> _payoutHandlers;
|
||||
private readonly StoreRepository _storeRepository;
|
||||
private readonly BTCPayServerEnvironment _env;
|
||||
private readonly SettingsRepository _settingsRepository;
|
||||
|
||||
public UIPullPaymentController(ApplicationDbContextFactory dbContextFactory,
|
||||
CurrencyNameTable currencyNameTable,
|
||||
@@ -41,7 +53,9 @@ namespace BTCPayServer.Controllers
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
BTCPayNetworkJsonSerializerSettings serializerSettings,
|
||||
IEnumerable<IPayoutHandler> payoutHandlers,
|
||||
StoreRepository storeRepository)
|
||||
StoreRepository storeRepository,
|
||||
BTCPayServerEnvironment env,
|
||||
SettingsRepository settingsRepository)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_currencyNameTable = currencyNameTable;
|
||||
@@ -50,6 +64,8 @@ namespace BTCPayServer.Controllers
|
||||
_serializerSettings = serializerSettings;
|
||||
_payoutHandlers = payoutHandlers;
|
||||
_storeRepository = storeRepository;
|
||||
_env = env;
|
||||
_settingsRepository = settingsRepository;
|
||||
_networkProvider = networkProvider;
|
||||
}
|
||||
|
||||
@@ -225,10 +241,10 @@ namespace BTCPayServer.Controllers
|
||||
return await ViewPullPayment(pullPaymentId);
|
||||
}
|
||||
|
||||
var amtError = ClaimRequest.IsPayoutAmountOk(destination, vm.ClaimedAmount == 0? null: vm.ClaimedAmount, paymentMethodId.CryptoCode, ppBlob.Currency);
|
||||
var amtError = ClaimRequest.IsPayoutAmountOk(destination, vm.ClaimedAmount == 0 ? null : vm.ClaimedAmount, paymentMethodId.CryptoCode, ppBlob.Currency);
|
||||
if (amtError.error is not null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.ClaimedAmount), amtError.error );
|
||||
ModelState.AddModelError(nameof(vm.ClaimedAmount), amtError.error);
|
||||
}
|
||||
else if (amtError.amount is not null)
|
||||
{
|
||||
|
||||
@@ -16,7 +16,6 @@ using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Internal;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Threading;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
62
BTCPayServer/Data/BoltcardDataExtensions.cs
Normal file
62
BTCPayServer/Data/BoltcardDataExtensions.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
#nullable enable
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.NTag424;
|
||||
using Dapper;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NBitcoin.DataEncoders;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer;
|
||||
public static class BoltcardDataExtensions
|
||||
{
|
||||
public static async Task<int> LinkBoltcardToPullPayment(this ApplicationDbContextFactory dbContextFactory, string pullPaymentId, IssuerKey issuerKey, byte[] uid, OnExistingBehavior? onExisting = null)
|
||||
{
|
||||
onExisting ??= OnExistingBehavior.UpdateVersion;
|
||||
using var ctx = dbContextFactory.CreateContext();
|
||||
var conn = ctx.Database.GetDbConnection();
|
||||
|
||||
string onConflict = onExisting switch
|
||||
{
|
||||
OnExistingBehavior.KeepVersion => "UPDATE SET ppid=excluded.ppid, version=boltcards.version",
|
||||
OnExistingBehavior.UpdateVersion => "UPDATE SET ppid=excluded.ppid, version=excluded.version+1",
|
||||
_ => throw new NotSupportedException()
|
||||
};
|
||||
return await conn.QueryFirstOrDefaultAsync<int>(
|
||||
$"INSERT INTO boltcards(id, ppid) VALUES (@id, @ppid) ON CONFLICT (id) DO {onConflict} RETURNING version", new
|
||||
{
|
||||
id = GetId(issuerKey, uid),
|
||||
ppid = pullPaymentId
|
||||
});
|
||||
}
|
||||
public static async Task SetBoltcardResetState(this ApplicationDbContextFactory dbContextFactory, IssuerKey issuerKey, byte[] uid)
|
||||
{
|
||||
using var ctx = dbContextFactory.CreateContext();
|
||||
var conn = ctx.Database.GetDbConnection();
|
||||
await conn.ExecuteAsync("UPDATE boltcards SET ppid=NULL, counter=0 WHERE id=@id", new
|
||||
{
|
||||
id = GetId(issuerKey, uid)
|
||||
});
|
||||
}
|
||||
|
||||
static string GetId(IssuerKey issuerKey, byte[] uid) => Encoders.Hex.EncodeData(issuerKey.GetId(uid));
|
||||
public record BoltcardRegistration(string? PullPaymentId, string Id, byte[] UId, int Version, int Counter);
|
||||
public static async Task<BoltcardRegistration?> GetBoltcardRegistration(this ApplicationDbContextFactory dbContextFactory, IssuerKey issuerKey, BoltcardPICCData piccData, bool updateCounter)
|
||||
{
|
||||
using var ctx = dbContextFactory.CreateContext();
|
||||
var conn = ctx.Database.GetDbConnection();
|
||||
var query = updateCounter ? "UPDATE boltcards SET counter=@counter WHERE id=@id AND counter < @counter RETURNING ppid, id, version, counter"
|
||||
: "SELECT ppid, id, version, counter FROM boltcards WHERE id=@id AND counter < @counter";
|
||||
var res = await conn.QueryFirstOrDefaultAsync(query, new { id = GetId(issuerKey, piccData.Uid), counter = piccData.Counter });
|
||||
if (res is null)
|
||||
return null;
|
||||
return new BoltcardRegistration(res.ppid, res.id, piccData.Uid, res.version, res.counter);
|
||||
}
|
||||
public static Task<BoltcardRegistration?> GetBoltcardRegistration(this ApplicationDbContextFactory dbContextFactory, IssuerKey issuerKey, byte[] uid)
|
||||
{
|
||||
var data = new BoltcardPICCData(uid, int.MaxValue);
|
||||
return GetBoltcardRegistration(dbContextFactory, issuerKey, data, false);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.NTag424;
|
||||
using BTCPayServer.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
|
||||
32
BTCPayServer/Extensions/SettingsRepositoryExtensions.cs
Normal file
32
BTCPayServer/Extensions/SettingsRepositoryExtensions.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
#nullable enable
|
||||
using BTCPayServer.NTag424;
|
||||
using BTCPayServer.Services;
|
||||
using static BTCPayServer.Controllers.UIBoltcardController;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer;
|
||||
public static class SettingsRepositoryExtensions
|
||||
{
|
||||
public static async Task<IssuerKey> GetIssuerKey(this SettingsRepository settingsRepository, BTCPayServerEnvironment env)
|
||||
{
|
||||
var settings = await settingsRepository.GetSettingAsync<BoltcardSettings>(nameof(BoltcardSettings));
|
||||
AESKey issuerKey;
|
||||
if (settings?.IssuerKey is byte[] bytes)
|
||||
{
|
||||
issuerKey = new AESKey(bytes);
|
||||
}
|
||||
else
|
||||
{
|
||||
issuerKey = env.CheatMode && env.IsDeveloping ? FixedKey() : AESKey.Random();
|
||||
settings = new BoltcardSettings() { IssuerKey = issuerKey.ToBytes() };
|
||||
await settingsRepository.UpdateSetting(settings, nameof(BoltcardSettings));
|
||||
}
|
||||
return new IssuerKey(issuerKey);
|
||||
}
|
||||
internal static AESKey FixedKey()
|
||||
{
|
||||
byte[] v = new byte[16];
|
||||
v[0] = 1;
|
||||
return new AESKey(v);
|
||||
}
|
||||
}
|
||||
9
BTCPayServer/Models/SetupBoltcardViewModel.cs
Normal file
9
BTCPayServer/Models/SetupBoltcardViewModel.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace BTCPayServer.Models
|
||||
{
|
||||
public class SetupBoltcardViewModel
|
||||
{
|
||||
public string ReturnUrl { get; set; }
|
||||
public string WebsocketPath { get; set; }
|
||||
public string Command { get; set; }
|
||||
}
|
||||
}
|
||||
83
BTCPayServer/Views/UIPullPayment/SetupBoltcard.cshtml
Normal file
83
BTCPayServer/Views/UIPullPayment/SetupBoltcard.cshtml
Normal file
@@ -0,0 +1,83 @@
|
||||
@using BTCPayServer.Controllers
|
||||
@model SetupBoltcardViewModel
|
||||
@{
|
||||
Layout = "_LayoutWizard";
|
||||
}
|
||||
|
||||
@section Navbar {
|
||||
<a href="@Url.EnsureLocal(Model.ReturnUrl, Context.Request)" id="CancelWizard" class="cancel">
|
||||
<vc:icon symbol="close" />
|
||||
</a>
|
||||
}
|
||||
|
||||
<header class="text-center">
|
||||
<h1>@ViewData["Title"]</h1>
|
||||
<p class="lead text-secondary mt-3">Using BTCPay Server Vault (NFC)</p>
|
||||
</header>
|
||||
|
||||
<div id="walletAlert" class="alert alert-warning alert-dismissible my-4" style="display:none;" role="alert">
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close">
|
||||
<vc:icon symbol="close" />
|
||||
</button>
|
||||
<span id="alertMessage"></span>
|
||||
</div>
|
||||
|
||||
<div id="body" class="my-4">
|
||||
<form id="broadcastForm" method="post" style="display:none;">
|
||||
<input type="hidden" asp-for="WebsocketPath" />
|
||||
<input type="hidden" asp-for="ReturnUrl" />
|
||||
</form>
|
||||
<div id="vaultPlaceholder"></div>
|
||||
<button id="vault-retry" class="btn btn-primary" style="display:none;" type="button">Retry</button>
|
||||
<button id="vault-confirm" class="btn btn-primary" style="display:none;"></button>
|
||||
</div>
|
||||
|
||||
<partial name="VaultElements" />
|
||||
|
||||
@section PageFootContent
|
||||
{
|
||||
<script src="~/js/vaultbridge.js" type="text/javascript" defer="defer" asp-append-version="true"></script>
|
||||
<script src="~/js/vaultbridge.ui.js" type="text/javascript" defer="defer" asp-append-version="true"></script>
|
||||
<script>
|
||||
function delay(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
async function askSign() {
|
||||
var websocketPath = $("#WebsocketPath").val();
|
||||
var loc = window.location, ws_uri;
|
||||
if (loc.protocol === "https:") {
|
||||
ws_uri = "wss:";
|
||||
} else {
|
||||
ws_uri = "ws:";
|
||||
}
|
||||
ws_uri += "//" + loc.host;
|
||||
ws_uri += websocketPath;
|
||||
var html = $("#VaultConnection").html();
|
||||
$("#vaultPlaceholder").html(html);
|
||||
var vaultUI = new vaultui.VaultBridgeUI(ws_uri);
|
||||
var command = @Safe.Json(Model.Command);
|
||||
while (!await vaultUI.sendBackendCommand(command)) {
|
||||
await vaultUI.waitRetryPushed();
|
||||
}
|
||||
await delay(2000);
|
||||
$("#broadcastForm").submit();
|
||||
}
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
askSign();
|
||||
});
|
||||
var alertMsg = document.getElementById("alertMessage");
|
||||
var walletAlert = document.getElementById("walletAlert");
|
||||
var isSafari = window.safari !== undefined;
|
||||
if (isSafari)
|
||||
{
|
||||
alertMsg.innerHTML = "Safari doesn't support BTCPay Server Vault. Please use a different browser. (<a class=\"alert-link\" href=\"https://bugs.webkit.org/show_bug.cgi?id=171934\" target=\"_blank\" rel=\"noreferrer noopener\">More information</a>)";
|
||||
walletAlert.style.display = null;
|
||||
}
|
||||
var isBrave = navigator.brave !== undefined;
|
||||
if (isBrave)
|
||||
{
|
||||
alertMsg.innerHTML = "Brave supports BTCPay Server Vault, but you need to disable Brave Shields. (<a class=\"alert-link\" href=\"https://www.updateland.com/how-to-turn-off-brave-shields/\" target=\"_blank\" rel=\"noreferrer noopener\">More information</a>)";
|
||||
walletAlert.style.display = null;
|
||||
}
|
||||
</script>
|
||||
}
|
||||
@@ -31,7 +31,9 @@
|
||||
<link href="~/vendor/bootstrap-vue/bootstrap-vue.min.css" asp-append-version="true" rel="stylesheet"/>
|
||||
<link href="~/vendor/vue-qrcode-reader/vue-qrcode-reader.css" rel="stylesheet" asp-append-version="true"/>
|
||||
<style>
|
||||
.no-marker > ul { list-style-type: none; }
|
||||
.no-marker > ul {
|
||||
list-style-type: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="min-vh-100">
|
||||
@@ -203,13 +205,25 @@
|
||||
Edit pull payment
|
||||
</a>
|
||||
</p>
|
||||
@if (Model.LnurlEndpoint is not null)
|
||||
{
|
||||
<p>
|
||||
<a asp-action="SetupBoltcard" asp-controller="UIPullPayment" asp-route-pullPaymentId="@Model.Id" asp-route-command="configure-boltcard">
|
||||
Setup Boltcard
|
||||
</a>
|
||||
<span> | </span>
|
||||
<a asp-action="SetupBoltcard" asp-controller="UIPullPayment" asp-route-pullPaymentId="@Model.Id" asp-route-command="reset-boltcard">
|
||||
Reset Boltcard
|
||||
</a>
|
||||
</p>
|
||||
}
|
||||
<a class="store-powered-by" href="https://btcpayserver.org" target="_blank" rel="noreferrer noopener">
|
||||
Powered by <partial name="_StoreFooterLogo" />
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
<partial name="ShowQR" />
|
||||
<partial name="CameraScanner"/>
|
||||
<partial name="CameraScanner" />
|
||||
<partial name="LayoutFoot" />
|
||||
<script src="~/vendor/vuejs/vue.min.js" asp-append-version="true"></script>
|
||||
<script src="~/vendor/vue-qrcode/vue-qrcode.min.js" asp-append-version="true"></script>
|
||||
|
||||
@@ -60,12 +60,14 @@ var vaultui = (function () {
|
||||
* @type {VaultBridgeUI}
|
||||
*/
|
||||
var self = this;
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
this.backend_uri = backend_uri;
|
||||
/**
|
||||
* @type {vault.VaultBridge}
|
||||
*/
|
||||
this.bridge = null;
|
||||
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
@@ -127,6 +129,18 @@ var vaultui = (function () {
|
||||
console.warn(json.details);
|
||||
}
|
||||
}
|
||||
|
||||
function showMessage(message) {
|
||||
let type = 'ok';
|
||||
if (message.type === 'Error')
|
||||
type = 'failed';
|
||||
if (message.type === 'Processing')
|
||||
type = '?';
|
||||
show(new VaultFeedback(type, message.message, ""));
|
||||
if (type.debug)
|
||||
console.warn(type.debug);
|
||||
}
|
||||
|
||||
async function needRetry(json) {
|
||||
if (json.hasOwnProperty("error")) {
|
||||
var handled = false;
|
||||
@@ -212,7 +226,26 @@ var vaultui = (function () {
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
this.sendBackendCommand = async function (command) {
|
||||
if (!self.bridge || self.bridge.socket.readyState !== 1) {
|
||||
self.bridge = await vault.connectToBackendSocket(self.backend_uri);
|
||||
}
|
||||
show(VaultFeedbacks.vaultLoading);
|
||||
self.bridge.socket.send(command);
|
||||
while (true) {
|
||||
var json = await self.bridge.waitBackendMessage();
|
||||
if (json.command === 'showMessage') {
|
||||
showMessage(json);
|
||||
if (json.type === "Error") {
|
||||
showRetry();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (json.command == 'done') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.askForDisplayAddress = async function (rootedKeyPath) {
|
||||
if (!await self.ensureConnectedToBackend())
|
||||
return false;
|
||||
|
||||
@@ -1,5 +1,116 @@
|
||||
{
|
||||
"paths": {
|
||||
"/api/v1/pull-payments/{pullPaymentId}/boltcards": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "pullPaymentId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "The ID of the pull payment",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"post": {
|
||||
"operationId": "PullPayments_LinkBoltcard",
|
||||
"summary": "Link a boltcard to a pull payment",
|
||||
"description": "Linking a boltcard to a pull payment will allow you to pay via NFC with it, the money will be sent from the pull payment. The boltcard keys are generated using [Deterministic Boltcard Key Generation](https://github.com/boltcard/boltcard/blob/main/docs/DETERMINISTIC.md).",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"required": [ "UID" ],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"UID": {
|
||||
"type": "string",
|
||||
"example": "46ab87ff36a3b7",
|
||||
"description": "The `UID` of the NTag424",
|
||||
"nullable": false
|
||||
},
|
||||
"onExisting": {
|
||||
"type": "string",
|
||||
"x-enumNames": [
|
||||
"KeepVersion",
|
||||
"UpdateVersion"
|
||||
],
|
||||
"enum": [
|
||||
"KeepVersion",
|
||||
"UpdateVersion"
|
||||
],
|
||||
"default": "UpdateVersion",
|
||||
"description": "What to do if the boltcard is already linked.\n * `KeepVersion` will return the keys (K0-K4) that are already registered.\n * `UpdateVersion` will increment the version of the key, and thus return different keys (K0-K4). (See [Deterministic Boltcard Key Generation](https://github.com/boltcard/boltcard/blob/main/docs/DETERMINISTIC.md))"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The boltcard has been linked to the pull payment.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"LNURLW": {
|
||||
"type": "string",
|
||||
"description": "The lnurl withdraw of the server",
|
||||
"example": "lnurlw://example.com/boltcard"
|
||||
},
|
||||
"version": {
|
||||
"type": "number",
|
||||
"description": "The version of the registration (See [Deterministic Boltcard Key Generation](https://github.com/boltcard/boltcard/blob/main/docs/DETERMINISTIC.md))"
|
||||
},
|
||||
"K0": {
|
||||
"type": "string",
|
||||
"description": "The public key K0 of the boltcard",
|
||||
"example": "02a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b"
|
||||
},
|
||||
"K1": {
|
||||
"type": "string",
|
||||
"description": "The public key K1 of the boltcard",
|
||||
"example": "02a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b"
|
||||
},
|
||||
"K2": {
|
||||
"type": "string",
|
||||
"description": "The public key K2 of the boltcard",
|
||||
"example": "02a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b"
|
||||
},
|
||||
"K3": {
|
||||
"type": "string",
|
||||
"description": "The public key K3 of the boltcard",
|
||||
"example": "02a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b"
|
||||
},
|
||||
"K4": {
|
||||
"type": "string",
|
||||
"description": "The public key K4 of the boltcard",
|
||||
"example": "02a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b"
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "The pull payment has not been found. Wellknown error code is: `pullpayment-not-found`",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Pull payments (Public)"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v1/stores/{storeId}/pull-payments": {
|
||||
"parameters": [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user