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);
|
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)
|
public virtual async Task<PullPaymentData[]> GetPullPayments(string storeId, bool includeArchived = false, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
Dictionary<string, object> query = new Dictionary<string, object>();
|
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.Infrastructure;
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
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();
|
driver.FindElement(selector).Click();
|
||||||
}
|
}
|
||||||
|
|
||||||
[DebuggerHidden]
|
|
||||||
public static bool ElementDoesNotExist(this IWebDriver driver, By selector)
|
public static bool ElementDoesNotExist(this IWebDriver driver, By selector)
|
||||||
{
|
{
|
||||||
Assert.Throws<NoSuchElementException>(() =>
|
Assert.Throws<NoSuchElementException>(
|
||||||
|
[DebuggerStepThrough]
|
||||||
|
() =>
|
||||||
{
|
{
|
||||||
driver.FindElement(selector);
|
driver.FindElement(selector);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
|
using System.Security.Cryptography;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BTCPayServer.Abstractions.Contracts;
|
using BTCPayServer.Abstractions.Contracts;
|
||||||
@@ -1084,7 +1085,39 @@ namespace BTCPayServer.Tests
|
|||||||
Assert.IsType<string>(lnrURLs.LNURLUri);
|
Assert.IsType<string>(lnrURLs.LNURLUri);
|
||||||
Assert.Equal(12.303228134m, test4.Amount);
|
Assert.Equal(12.303228134m, test4.Amount);
|
||||||
Assert.Equal("BTC", test4.Currency);
|
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
|
// Test with SATS denomination values
|
||||||
var testSats = await client.CreatePullPayment(storeId, new Client.Models.CreatePullPaymentRequest()
|
var testSats = await client.CreatePullPayment(storeId, new Client.Models.CreatePullPaymentRequest()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using System.Globalization;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
@@ -17,6 +18,7 @@ using BTCPayServer.Controllers;
|
|||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
using BTCPayServer.Lightning;
|
using BTCPayServer.Lightning;
|
||||||
using BTCPayServer.Models.InvoicingModels;
|
using BTCPayServer.Models.InvoicingModels;
|
||||||
|
using BTCPayServer.NTag424;
|
||||||
using BTCPayServer.Payments;
|
using BTCPayServer.Payments;
|
||||||
using BTCPayServer.Services;
|
using BTCPayServer.Services;
|
||||||
using BTCPayServer.Services.Invoices;
|
using BTCPayServer.Services.Invoices;
|
||||||
@@ -25,6 +27,8 @@ using BTCPayServer.Views.Manage;
|
|||||||
using BTCPayServer.Views.Server;
|
using BTCPayServer.Views.Server;
|
||||||
using BTCPayServer.Views.Stores;
|
using BTCPayServer.Views.Stores;
|
||||||
using BTCPayServer.Views.Wallets;
|
using BTCPayServer.Views.Wallets;
|
||||||
|
using Dapper;
|
||||||
|
using ExchangeSharp;
|
||||||
using LNURL;
|
using LNURL;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@@ -2116,6 +2120,7 @@ namespace BTCPayServer.Tests
|
|||||||
s.Driver.FindElement(By.CssSelector("#lnurlwithdraw-button")).Click();
|
s.Driver.FindElement(By.CssSelector("#lnurlwithdraw-button")).Click();
|
||||||
s.Driver.WaitForElement(By.Id("qr-code-data-input"));
|
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"));
|
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();
|
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));
|
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));
|
Assert.Equal(info.CurrentBalance, new LightMoney(0.0000001m, LightMoneyUnit.BTC));
|
||||||
|
|
||||||
var bolt2 = (await s.Server.CustomerLightningD.CreateInvoice(
|
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}",
|
$"LNurl w payout test {DateTime.UtcNow.Ticks}",
|
||||||
TimeSpan.FromHours(1), CancellationToken.None));
|
TimeSpan.FromHours(1), CancellationToken.None));
|
||||||
var response = await info.SendRequest(bolt2.BOLT11, s.Server.PayTester.HttpClient, null,null);
|
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.Close();
|
||||||
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
|
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.GoToStore(s.StoreId, StoreNavPages.PullPayments);
|
||||||
s.Driver.FindElement(By.Id("NewPullPayment")).Click();
|
s.Driver.FindElement(By.Id("NewPullPayment")).Click();
|
||||||
s.Driver.FindElement(By.Id("Name")).SendKeys("PP1");
|
s.Driver.FindElement(By.Id("Name")).SendKeys("PP1");
|
||||||
@@ -2221,6 +2272,12 @@ namespace BTCPayServer.Tests
|
|||||||
s.Driver.Close();
|
s.Driver.Close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string RandomBytes(int count)
|
||||||
|
{
|
||||||
|
var c = RandomNumberGenerator.GetBytes(count);
|
||||||
|
return Encoders.Hex.EncodeData(c);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
[Trait("Selenium", "Selenium")]
|
[Trait("Selenium", "Selenium")]
|
||||||
[Trait("Lightning", "Lightning")]
|
[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>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="BTCPayServer.NTag424" Version="1.0.17" />
|
||||||
<PackageReference Include="YamlDotNet" Version="8.0.0" />
|
<PackageReference Include="YamlDotNet" Version="8.0.0" />
|
||||||
<PackageReference Include="BIP78.Sender" Version="0.2.2" />
|
<PackageReference Include="BIP78.Sender" Version="0.2.2" />
|
||||||
<PackageReference Include="BTCPayServer.Hwi" Version="2.0.2" />
|
<PackageReference Include="BTCPayServer.Hwi" Version="2.0.2" />
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BTCPayServer.Abstractions.Constants;
|
using BTCPayServer.Abstractions.Constants;
|
||||||
@@ -10,6 +11,7 @@ using BTCPayServer.Client;
|
|||||||
using BTCPayServer.Client.Models;
|
using BTCPayServer.Client.Models;
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
using BTCPayServer.HostedServices;
|
using BTCPayServer.HostedServices;
|
||||||
|
using BTCPayServer.NTag424;
|
||||||
using BTCPayServer.Payments;
|
using BTCPayServer.Payments;
|
||||||
using BTCPayServer.Security;
|
using BTCPayServer.Security;
|
||||||
using BTCPayServer.Services;
|
using BTCPayServer.Services;
|
||||||
@@ -19,6 +21,7 @@ using Microsoft.AspNetCore.Cors;
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.Routing;
|
using Microsoft.AspNetCore.Routing;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using NBitcoin.DataEncoders;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using MarkPayoutRequest = BTCPayServer.HostedServices.MarkPayoutRequest;
|
using MarkPayoutRequest = BTCPayServer.HostedServices.MarkPayoutRequest;
|
||||||
|
|
||||||
@@ -37,6 +40,8 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||||||
private readonly IEnumerable<IPayoutHandler> _payoutHandlers;
|
private readonly IEnumerable<IPayoutHandler> _payoutHandlers;
|
||||||
private readonly BTCPayNetworkProvider _networkProvider;
|
private readonly BTCPayNetworkProvider _networkProvider;
|
||||||
private readonly IAuthorizationService _authorizationService;
|
private readonly IAuthorizationService _authorizationService;
|
||||||
|
private readonly SettingsRepository _settingsRepository;
|
||||||
|
private readonly BTCPayServerEnvironment _env;
|
||||||
|
|
||||||
public GreenfieldPullPaymentController(PullPaymentHostedService pullPaymentService,
|
public GreenfieldPullPaymentController(PullPaymentHostedService pullPaymentService,
|
||||||
LinkGenerator linkGenerator,
|
LinkGenerator linkGenerator,
|
||||||
@@ -45,7 +50,9 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||||||
Services.BTCPayNetworkJsonSerializerSettings serializerSettings,
|
Services.BTCPayNetworkJsonSerializerSettings serializerSettings,
|
||||||
IEnumerable<IPayoutHandler> payoutHandlers,
|
IEnumerable<IPayoutHandler> payoutHandlers,
|
||||||
BTCPayNetworkProvider btcPayNetworkProvider,
|
BTCPayNetworkProvider btcPayNetworkProvider,
|
||||||
IAuthorizationService authorizationService)
|
IAuthorizationService authorizationService,
|
||||||
|
SettingsRepository settingsRepository,
|
||||||
|
BTCPayServerEnvironment env)
|
||||||
{
|
{
|
||||||
_pullPaymentService = pullPaymentService;
|
_pullPaymentService = pullPaymentService;
|
||||||
_linkGenerator = linkGenerator;
|
_linkGenerator = linkGenerator;
|
||||||
@@ -55,6 +62,8 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||||||
_payoutHandlers = payoutHandlers;
|
_payoutHandlers = payoutHandlers;
|
||||||
_networkProvider = btcPayNetworkProvider;
|
_networkProvider = btcPayNetworkProvider;
|
||||||
_authorizationService = authorizationService;
|
_authorizationService = authorizationService;
|
||||||
|
_settingsRepository = settingsRepository;
|
||||||
|
_env = env;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("~/api/v1/stores/{storeId}/pull-payments")]
|
[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}")]
|
[HttpGet("~/api/v1/pull-payments/{pullPaymentId}")]
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
public async Task<IActionResult> GetPullPayment(string pullPaymentId)
|
public async Task<IActionResult> GetPullPayment(string pullPaymentId)
|
||||||
|
|||||||
@@ -1275,6 +1275,11 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||||||
return GetFromActionResult<PayoutData>(await GetController<GreenfieldPullPaymentController>().GetPayout(pullPaymentId, payoutId));
|
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)
|
public override async Task<PullPaymentLNURL> GetPullPaymentLNURL(string pullPaymentId, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
return GetFromActionResult<PullPaymentLNURL>(await GetController<GreenfieldPullPaymentController>().GetPullPaymentLNURL(pullPaymentId));
|
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;
|
_pluginHookService = pluginHookService;
|
||||||
_invoiceActivator = invoiceActivator;
|
_invoiceActivator = invoiceActivator;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("withdraw/pp/{pullPaymentId}")]
|
[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);
|
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
|
||||||
if (network is null || !network.SupportLightning)
|
if (network is null || !network.SupportLightning)
|
||||||
@@ -119,7 +123,7 @@ namespace BTCPayServer
|
|||||||
var request = new LNURLWithdrawRequest
|
var request = new LNURLWithdrawRequest
|
||||||
{
|
{
|
||||||
MaxWithdrawable = LightMoney.FromUnit(remaining, unit),
|
MaxWithdrawable = LightMoney.FromUnit(remaining, unit),
|
||||||
K1 = pullPaymentId,
|
K1 = k1,
|
||||||
BalanceCheck = new Uri(Request.GetCurrentUrl()),
|
BalanceCheck = new Uri(Request.GetCurrentUrl()),
|
||||||
CurrentBalance = LightMoney.FromUnit(remaining, unit),
|
CurrentBalance = LightMoney.FromUnit(remaining, unit),
|
||||||
MinWithdrawable =
|
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;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Amazon.S3.Model;
|
||||||
using BTCPayServer.Abstractions.Constants;
|
using BTCPayServer.Abstractions.Constants;
|
||||||
using BTCPayServer.Abstractions.Extensions;
|
using BTCPayServer.Abstractions.Extensions;
|
||||||
using BTCPayServer.Abstractions.Models;
|
using BTCPayServer.Abstractions.Models;
|
||||||
@@ -10,20 +13,27 @@ using BTCPayServer.Client;
|
|||||||
using BTCPayServer.Client.Models;
|
using BTCPayServer.Client.Models;
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
using BTCPayServer.HostedServices;
|
using BTCPayServer.HostedServices;
|
||||||
|
using BTCPayServer.Lightning;
|
||||||
|
using BTCPayServer.ModelBinders;
|
||||||
using BTCPayServer.Models;
|
using BTCPayServer.Models;
|
||||||
using BTCPayServer.Models.WalletViewModels;
|
using BTCPayServer.Models.WalletViewModels;
|
||||||
|
using BTCPayServer.NTag424;
|
||||||
using BTCPayServer.Payments;
|
using BTCPayServer.Payments;
|
||||||
using BTCPayServer.Services;
|
using BTCPayServer.Services;
|
||||||
using BTCPayServer.Services.Rates;
|
using BTCPayServer.Services.Rates;
|
||||||
using BTCPayServer.Services.Stores;
|
using BTCPayServer.Services.Stores;
|
||||||
|
using Dapper;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NBitcoin;
|
using NBitcoin;
|
||||||
|
using NBitcoin.DataEncoders;
|
||||||
|
using NdefLibrary.Ndef;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
namespace BTCPayServer.Controllers
|
namespace BTCPayServer.Controllers
|
||||||
{
|
{
|
||||||
public class UIPullPaymentController : Controller
|
public partial class UIPullPaymentController : Controller
|
||||||
{
|
{
|
||||||
private readonly ApplicationDbContextFactory _dbContextFactory;
|
private readonly ApplicationDbContextFactory _dbContextFactory;
|
||||||
private readonly CurrencyNameTable _currencyNameTable;
|
private readonly CurrencyNameTable _currencyNameTable;
|
||||||
@@ -33,6 +43,8 @@ namespace BTCPayServer.Controllers
|
|||||||
private readonly BTCPayNetworkJsonSerializerSettings _serializerSettings;
|
private readonly BTCPayNetworkJsonSerializerSettings _serializerSettings;
|
||||||
private readonly IEnumerable<IPayoutHandler> _payoutHandlers;
|
private readonly IEnumerable<IPayoutHandler> _payoutHandlers;
|
||||||
private readonly StoreRepository _storeRepository;
|
private readonly StoreRepository _storeRepository;
|
||||||
|
private readonly BTCPayServerEnvironment _env;
|
||||||
|
private readonly SettingsRepository _settingsRepository;
|
||||||
|
|
||||||
public UIPullPaymentController(ApplicationDbContextFactory dbContextFactory,
|
public UIPullPaymentController(ApplicationDbContextFactory dbContextFactory,
|
||||||
CurrencyNameTable currencyNameTable,
|
CurrencyNameTable currencyNameTable,
|
||||||
@@ -41,7 +53,9 @@ namespace BTCPayServer.Controllers
|
|||||||
BTCPayNetworkProvider networkProvider,
|
BTCPayNetworkProvider networkProvider,
|
||||||
BTCPayNetworkJsonSerializerSettings serializerSettings,
|
BTCPayNetworkJsonSerializerSettings serializerSettings,
|
||||||
IEnumerable<IPayoutHandler> payoutHandlers,
|
IEnumerable<IPayoutHandler> payoutHandlers,
|
||||||
StoreRepository storeRepository)
|
StoreRepository storeRepository,
|
||||||
|
BTCPayServerEnvironment env,
|
||||||
|
SettingsRepository settingsRepository)
|
||||||
{
|
{
|
||||||
_dbContextFactory = dbContextFactory;
|
_dbContextFactory = dbContextFactory;
|
||||||
_currencyNameTable = currencyNameTable;
|
_currencyNameTable = currencyNameTable;
|
||||||
@@ -50,6 +64,8 @@ namespace BTCPayServer.Controllers
|
|||||||
_serializerSettings = serializerSettings;
|
_serializerSettings = serializerSettings;
|
||||||
_payoutHandlers = payoutHandlers;
|
_payoutHandlers = payoutHandlers;
|
||||||
_storeRepository = storeRepository;
|
_storeRepository = storeRepository;
|
||||||
|
_env = env;
|
||||||
|
_settingsRepository = settingsRepository;
|
||||||
_networkProvider = networkProvider;
|
_networkProvider = networkProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,7 +130,7 @@ namespace BTCPayServer.Controllers
|
|||||||
var url = Url.Action("GetLNURLForPullPayment", "UILNURL", new { cryptoCode = _networkProvider.DefaultNetwork.CryptoCode, pullPaymentId = vm.Id }, Request.Scheme, Request.Host.ToString());
|
var url = Url.Action("GetLNURLForPullPayment", "UILNURL", new { cryptoCode = _networkProvider.DefaultNetwork.CryptoCode, pullPaymentId = vm.Id }, Request.Scheme, Request.Host.ToString());
|
||||||
vm.LnurlEndpoint = url != null ? new Uri(url) : null;
|
vm.LnurlEndpoint = url != null ? new Uri(url) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return View(nameof(ViewPullPayment), vm);
|
return View(nameof(ViewPullPayment), vm);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,11 +240,11 @@ namespace BTCPayServer.Controllers
|
|||||||
ModelState.AddModelError(nameof(vm.Destination), "Invalid destination or payment method");
|
ModelState.AddModelError(nameof(vm.Destination), "Invalid destination or payment method");
|
||||||
return await ViewPullPayment(pullPaymentId);
|
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)
|
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)
|
else if (amtError.amount is not null)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ using Microsoft.AspNetCore.Authorization;
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Internal;
|
using Microsoft.EntityFrameworkCore.Internal;
|
||||||
using System.Text.Json.Nodes;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Newtonsoft.Json.Linq;
|
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 System.Threading.Tasks;
|
||||||
using BTCPayServer.Configuration;
|
using BTCPayServer.Configuration;
|
||||||
using BTCPayServer.Logging;
|
using BTCPayServer.Logging;
|
||||||
|
using BTCPayServer.NTag424;
|
||||||
using BTCPayServer.Services;
|
using BTCPayServer.Services;
|
||||||
using Microsoft.Extensions.Logging;
|
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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
<div class="vault-feedback vault-feedback3 mb-2 d-flex">
|
<div class="vault-feedback vault-feedback3 mb-2 d-flex">
|
||||||
<span class="vault-feedback-icon mt-1 me-2"></span> <span class="vault-feedback-content flex-grow"></span>
|
<span class="vault-feedback-icon mt-1 me-2"></span> <span class="vault-feedback-content flex-grow"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="vault-feedback vault-feedback4 mb-2 d-flex">
|
<div class="vault-feedback vault-feedback4 mb-2 d-flex">
|
||||||
<span class="vault-feedback-icon mt-1 me-2"></span> <span class="vault-feedback-content flex-grow"></span>
|
<span class="vault-feedback-icon mt-1 me-2"></span> <span class="vault-feedback-content flex-grow"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="vault-feedback vault-feedback5 mb-2 d-flex">
|
<div class="vault-feedback vault-feedback5 mb-2 d-flex">
|
||||||
|
|||||||
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/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"/>
|
<link href="~/vendor/vue-qrcode-reader/vue-qrcode-reader.css" rel="stylesheet" asp-append-version="true"/>
|
||||||
<style>
|
<style>
|
||||||
.no-marker > ul { list-style-type: none; }
|
.no-marker > ul {
|
||||||
|
list-style-type: none;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body class="min-vh-100">
|
<body class="min-vh-100">
|
||||||
@@ -156,34 +158,34 @@
|
|||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table my-0">
|
<table class="table my-0">
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="table-borderless">
|
<tr class="table-borderless">
|
||||||
<th class="fw-normal text-secondary" scope="col">Destination</th>
|
<th class="fw-normal text-secondary" scope="col">Destination</th>
|
||||||
<th class="fw-normal text-secondary" scope="col">Method</th>
|
<th class="fw-normal text-secondary" scope="col">Method</th>
|
||||||
<th class="fw-normal text-secondary text-end text-nowrap">Amount requested</th>
|
<th class="fw-normal text-secondary text-end text-nowrap">Amount requested</th>
|
||||||
<th class="fw-normal text-secondary text-end">Status</th>
|
<th class="fw-normal text-secondary text-end">Status</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@foreach (var invoice in Model.Payouts)
|
@foreach (var invoice in Model.Payouts)
|
||||||
{
|
{
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-break">
|
<td class="text-break">
|
||||||
@invoice.Destination
|
@invoice.Destination
|
||||||
</td>
|
</td>
|
||||||
<td class="text-nowrap">@invoice.PaymentMethod.ToPrettyString()</td>
|
<td class="text-nowrap">@invoice.PaymentMethod.ToPrettyString()</td>
|
||||||
<td class="text-end text-nowrap">@invoice.AmountFormatted</td>
|
<td class="text-end text-nowrap">@invoice.AmountFormatted</td>
|
||||||
<td class="text-end text-nowrap">
|
<td class="text-end text-nowrap">
|
||||||
@if (!string.IsNullOrEmpty(invoice.Link))
|
@if (!string.IsNullOrEmpty(invoice.Link))
|
||||||
{
|
{
|
||||||
<a class="transaction-link text-white badge @StatusTextClass(invoice.Status.ToString())" href="@invoice.Link" rel="noreferrer noopener">@invoice.Status.GetStateString()</a>
|
<a class="transaction-link text-white badge @StatusTextClass(invoice.Status.ToString())" href="@invoice.Link" rel="noreferrer noopener">@invoice.Status.GetStateString()</a>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<span class="text-white badge @StatusTextClass(invoice.Status.ToString())">@invoice.Status.GetStateString()</span>
|
<span class="text-white badge @StatusTextClass(invoice.Status.ToString())">@invoice.Status.GetStateString()</span>
|
||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@@ -203,13 +205,25 @@
|
|||||||
Edit pull payment
|
Edit pull payment
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</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">
|
<a class="store-powered-by" href="https://btcpayserver.org" target="_blank" rel="noreferrer noopener">
|
||||||
Powered by <partial name="_StoreFooterLogo" />
|
Powered by <partial name="_StoreFooterLogo" />
|
||||||
</a>
|
</a>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
<partial name="ShowQR" />
|
<partial name="ShowQR" />
|
||||||
<partial name="CameraScanner"/>
|
<partial name="CameraScanner" />
|
||||||
<partial name="LayoutFoot" />
|
<partial name="LayoutFoot" />
|
||||||
<script src="~/vendor/vuejs/vue.min.js" asp-append-version="true"></script>
|
<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>
|
<script src="~/vendor/vue-qrcode/vue-qrcode.min.js" asp-append-version="true"></script>
|
||||||
@@ -223,39 +237,39 @@
|
|||||||
qrApp.note = "Scan this QR code to open this page on your mobile device.";
|
qrApp.note = "Scan this QR code to open this page on your mobile device.";
|
||||||
qrApp.showData(window.location.href);
|
qrApp.showData(window.location.href);
|
||||||
});
|
});
|
||||||
|
|
||||||
delegate('click', '#copyLink', window.copyUrlToClipboard);
|
delegate('click', '#copyLink', window.copyUrlToClipboard);
|
||||||
|
|
||||||
initCameraScanningApp("Scan address/ payment link", data => {
|
initCameraScanningApp("Scan address/ payment link", data => {
|
||||||
document.getElementById("Destination").value = data;
|
document.getElementById("Destination").value = data;
|
||||||
}, "scanModal");
|
}, "scanModal");
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@if (Model.LnurlEndpoint is not null)
|
@if (Model.LnurlEndpoint is not null)
|
||||||
{
|
{
|
||||||
var lnurlUri = LNURL.LNURL.EncodeUri(Model.LnurlEndpoint, "withdrawRequest", false).ToString();
|
var lnurlUri = LNURL.LNURL.EncodeUri(Model.LnurlEndpoint, "withdrawRequest", false).ToString();
|
||||||
var lnurlBech32 = LNURL.LNURL.EncodeUri(Model.LnurlEndpoint, "withdrawRequest", true).ToString();
|
var lnurlBech32 = LNURL.LNURL.EncodeUri(Model.LnurlEndpoint, "withdrawRequest", true).ToString();
|
||||||
var note = "<p>You can scan or open this link with a <a href='https://github.com/fiatjaf/lnurl-rfc#lnurl-documents' target='_blank' rel='noreferrer noopener'>LNURL-Withdraw</a> enabled wallet.</p>";
|
var note = "<p>You can scan or open this link with a <a href='https://github.com/fiatjaf/lnurl-rfc#lnurl-documents' target='_blank' rel='noreferrer noopener'>LNURL-Withdraw</a> enabled wallet.</p>";
|
||||||
if (!Model.AutoApprove)
|
if (!Model.AutoApprove)
|
||||||
{
|
{
|
||||||
note += "<p class='fw-semibold'>Please note that this pull payment does not automatically send out funds, but will process the payment after the LNURL-withdraw flow is completed.</p>";
|
note += "<p class='fw-semibold'>Please note that this pull payment does not automatically send out funds, but will process the payment after the LNURL-withdraw flow is completed.</p>";
|
||||||
}
|
}
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
const modes = {
|
const modes = {
|
||||||
uri: { title: "URI", fragments: [@Safe.Json(lnurlUri)], showData: true, href: @Safe.Json(lnurlUri) },
|
uri: { title: "URI", fragments: [@Safe.Json(lnurlUri)], showData: true, href: @Safe.Json(lnurlUri) },
|
||||||
bech32: { title: "Bech32", fragments: [@Safe.Json(lnurlBech32)], showData: true, href: @Safe.Json(lnurlBech32) }
|
bech32: { title: "Bech32", fragments: [@Safe.Json(lnurlBech32)], showData: true, href: @Safe.Json(lnurlBech32) }
|
||||||
};
|
};
|
||||||
delegate('click', '#lnurlwithdraw-button', () => {
|
delegate('click', '#lnurlwithdraw-button', () => {
|
||||||
qrApp.title = "LNURL Withdraw";
|
qrApp.title = "LNURL Withdraw";
|
||||||
qrApp.modes = modes;
|
qrApp.modes = modes;
|
||||||
qrApp.mode = "bech32";
|
qrApp.mode = "bech32";
|
||||||
qrApp.note = @Safe.Json(note);
|
qrApp.note = @Safe.Json(note);
|
||||||
qrApp.show();
|
qrApp.show();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
}
|
}
|
||||||
<vc:ui-extension-point location="pullpayment-foot" model="@Model"></vc:ui-extension-point>
|
<vc:ui-extension-point location="pullpayment-foot" model="@Model"></vc:ui-extension-point>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -60,12 +60,14 @@ var vaultui = (function () {
|
|||||||
* @type {VaultBridgeUI}
|
* @type {VaultBridgeUI}
|
||||||
*/
|
*/
|
||||||
var self = this;
|
var self = this;
|
||||||
|
/**
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
this.backend_uri = backend_uri;
|
this.backend_uri = backend_uri;
|
||||||
/**
|
/**
|
||||||
* @type {vault.VaultBridge}
|
* @type {vault.VaultBridge}
|
||||||
*/
|
*/
|
||||||
this.bridge = null;
|
this.bridge = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {string}
|
* @type {string}
|
||||||
*/
|
*/
|
||||||
@@ -127,6 +129,18 @@ var vaultui = (function () {
|
|||||||
console.warn(json.details);
|
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) {
|
async function needRetry(json) {
|
||||||
if (json.hasOwnProperty("error")) {
|
if (json.hasOwnProperty("error")) {
|
||||||
var handled = false;
|
var handled = false;
|
||||||
@@ -212,7 +226,26 @@ var vaultui = (function () {
|
|||||||
}
|
}
|
||||||
return true;
|
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) {
|
this.askForDisplayAddress = async function (rootedKeyPath) {
|
||||||
if (!await self.ensureConnectedToBackend())
|
if (!await self.ensureConnectedToBackend())
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -1,5 +1,116 @@
|
|||||||
{
|
{
|
||||||
"paths": {
|
"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": {
|
"/api/v1/stores/{storeId}/pull-payments": {
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user