mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-01-18 21:32:27 +01:00
Use library for Payjoin Sender (#2158)
* Use library for Payjoin Sender * update payjoin sender to use new package and reduce code * fix using statements
This commit is contained in:
parent
c9cfe5cc6e
commit
49ae62b02e
@ -8,10 +8,10 @@ using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Bitcoin;
|
||||
using BTCPayServer.Payments.PayJoin;
|
||||
using BTCPayServer.Payments.PayJoin.Sender;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
@ -19,6 +19,7 @@ using BTCPayServer.Tests.Logging;
|
||||
using BTCPayServer.Views.Wallets;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using NBitcoin;
|
||||
using BTCPayServer.BIP78.Sender;
|
||||
using NBitcoin.Payment;
|
||||
using NBitpayClient;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
@ -165,9 +166,7 @@ namespace BTCPayServer.Tests
|
||||
var cashCow = tester.ExplorerNode;
|
||||
cashCow.Generate(2); // get some money in case
|
||||
|
||||
var unsupportedFormats = Enum.GetValues(typeof(ScriptPubKeyType))
|
||||
.AssertType<ScriptPubKeyType[]>()
|
||||
.Where(type => !PayjoinClient.SupportedFormats.Contains(type));
|
||||
var unsupportedFormats = new[] {ScriptPubKeyType.Legacy};
|
||||
|
||||
|
||||
foreach (ScriptPubKeyType senderAddressType in Enum.GetValues(typeof(ScriptPubKeyType)))
|
||||
@ -220,7 +219,7 @@ namespace BTCPayServer.Tests
|
||||
var invoiceRepository = s.Server.PayTester.GetService<InvoiceRepository>();
|
||||
s.RegisterNewUser(true);
|
||||
|
||||
foreach (var format in PayjoinClient.SupportedFormats)
|
||||
foreach (var format in new []{ScriptPubKeyType.Segwit, ScriptPubKeyType.SegwitP2SH})
|
||||
{
|
||||
var receiver = s.CreateNewStore();
|
||||
var receiverSeed = s.GenerateWallet("BTC", "", true, true, format);
|
||||
@ -410,7 +409,7 @@ namespace BTCPayServer.Tests
|
||||
using var fakeServer = new FakeServer();
|
||||
await fakeServer.Start();
|
||||
var bip21 = new BitcoinUrlBuilder($"bitcoin:{paymentAddress}?pj={fakeServer.ServerUri}", Network.RegTest);
|
||||
var requesting = pjClient.RequestPayjoin(bip21, derivationSchemeSettings, psbt, default);
|
||||
var requesting = pjClient.RequestPayjoin(bip21, new PayjoinWallet(derivationSchemeSettings), psbt, default);
|
||||
var request = await fakeServer.GetNextRequest();
|
||||
Assert.Equal("1", request.Request.Query["v"][0]);
|
||||
Assert.Equal(changeIndex.ToString(), request.Request.Query["additionalfeeoutputindex"][0]);
|
||||
@ -426,7 +425,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.Contains("contribution is more than maxadditionalfeecontribution", ex.Message);
|
||||
|
||||
Logs.Tester.LogInformation("The payjoin receiver tries to change one of our output");
|
||||
requesting = pjClient.RequestPayjoin(bip21, derivationSchemeSettings, psbt, default);
|
||||
requesting = pjClient.RequestPayjoin(bip21, new PayjoinWallet(derivationSchemeSettings), psbt, default);
|
||||
request = await fakeServer.GetNextRequest();
|
||||
originalPSBT = await ParsePSBT(request);
|
||||
proposalTx = originalPSBT.GetGlobalTransaction();
|
||||
@ -437,7 +436,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.Contains("The receiver decreased the value of one", ex.Message);
|
||||
|
||||
Logs.Tester.LogInformation("The payjoin receiver tries to pocket the fee");
|
||||
requesting = pjClient.RequestPayjoin(bip21, derivationSchemeSettings, psbt, default);
|
||||
requesting = pjClient.RequestPayjoin(bip21, new PayjoinWallet(derivationSchemeSettings), psbt, default);
|
||||
request = await fakeServer.GetNextRequest();
|
||||
originalPSBT = await ParsePSBT(request);
|
||||
proposalTx = originalPSBT.GetGlobalTransaction();
|
||||
@ -448,7 +447,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.Contains("The receiver decreased absolute fee", ex.Message);
|
||||
|
||||
Logs.Tester.LogInformation("The payjoin receiver tries to remove one of our output");
|
||||
requesting = pjClient.RequestPayjoin(bip21, derivationSchemeSettings, psbt, default);
|
||||
requesting = pjClient.RequestPayjoin(bip21, new PayjoinWallet(derivationSchemeSettings), psbt, default);
|
||||
request = await fakeServer.GetNextRequest();
|
||||
originalPSBT = await ParsePSBT(request);
|
||||
proposalTx = originalPSBT.GetGlobalTransaction();
|
||||
@ -460,7 +459,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.Contains("Some of our outputs are not included in the proposal", ex.Message);
|
||||
|
||||
Logs.Tester.LogInformation("The payjoin receiver tries to change their own output");
|
||||
requesting = pjClient.RequestPayjoin(bip21, derivationSchemeSettings, psbt, default);
|
||||
requesting = pjClient.RequestPayjoin(bip21, new PayjoinWallet(derivationSchemeSettings), psbt, default);
|
||||
request = await fakeServer.GetNextRequest();
|
||||
originalPSBT = await ParsePSBT(request);
|
||||
proposalTx = originalPSBT.GetGlobalTransaction();
|
||||
@ -472,7 +471,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
Logs.Tester.LogInformation("The payjoin receiver tries to send money to himself");
|
||||
pjClient.MaxFeeBumpContribution = Money.Satoshis(1);
|
||||
requesting = pjClient.RequestPayjoin(bip21, derivationSchemeSettings, psbt, default);
|
||||
requesting = pjClient.RequestPayjoin(bip21, new PayjoinWallet(derivationSchemeSettings), psbt, default);
|
||||
request = await fakeServer.GetNextRequest();
|
||||
originalPSBT = await ParsePSBT(request);
|
||||
proposalTx = originalPSBT.GetGlobalTransaction();
|
||||
@ -486,7 +485,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
Logs.Tester.LogInformation("The payjoin receiver can't use additional fee without adding inputs");
|
||||
pjClient.MinimumFeeRate = new FeeRate(50m);
|
||||
requesting = pjClient.RequestPayjoin(bip21, derivationSchemeSettings, psbt, default);
|
||||
requesting = pjClient.RequestPayjoin(bip21, new PayjoinWallet(derivationSchemeSettings), psbt, default);
|
||||
request = await fakeServer.GetNextRequest();
|
||||
originalPSBT = await ParsePSBT(request);
|
||||
proposalTx = originalPSBT.GetGlobalTransaction();
|
||||
@ -528,7 +527,7 @@ namespace BTCPayServer.Tests
|
||||
psbt.SignAll(derivationSchemeSettings.AccountDerivation, alice.GenerateWalletResponseV.AccountHDKey, signingAccount.GetRootedKeyPath());
|
||||
var endpoint = TestAccount.GetPayjoinBitcoinUrl(invoice, Network.RegTest);
|
||||
pjClient.MaxFeeBumpContribution = Money.Satoshis(50);
|
||||
var proposal = await pjClient.RequestPayjoin(endpoint, derivationSchemeSettings, psbt, default);
|
||||
var proposal = await pjClient.RequestPayjoin(endpoint, new PayjoinWallet(derivationSchemeSettings), psbt, default);
|
||||
Assert.True(proposal.TryGetFee(out var newFee));
|
||||
Assert.Equal(Money.Satoshis(3001 + 50), newFee);
|
||||
proposal = proposal.SignAll(derivationSchemeSettings.AccountDerivation, alice.GenerateWalletResponseV.AccountHDKey, signingAccount.GetRootedKeyPath());
|
||||
@ -559,7 +558,7 @@ namespace BTCPayServer.Tests
|
||||
psbt.SignAll(derivationSchemeSettings.AccountDerivation, alice.GenerateWalletResponseV.AccountHDKey, signingAccount.GetRootedKeyPath());
|
||||
endpoint = TestAccount.GetPayjoinBitcoinUrl(invoice, Network.RegTest);
|
||||
pjClient.MinimumFeeRate = new FeeRate(100_000_000.2m);
|
||||
var ex2 = await Assert.ThrowsAsync<PayjoinReceiverException>(async () => await pjClient.RequestPayjoin(endpoint, derivationSchemeSettings, psbt, default));
|
||||
var ex2 = await Assert.ThrowsAsync<PayjoinReceiverException>(async () => await pjClient.RequestPayjoin(endpoint, new PayjoinWallet(derivationSchemeSettings), psbt, default));
|
||||
Assert.Equal(PayjoinReceiverWellknownErrors.NotEnoughMoney, ex2.WellknownError);
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ using BTCPayServer.Views.Stores;
|
||||
using BTCPayServer.Views.Wallets;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using NBitcoin;
|
||||
using BTCPayServer.BIP78.Sender;
|
||||
using OpenQA.Selenium;
|
||||
using OpenQA.Selenium.Chrome;
|
||||
using OpenQA.Selenium.Support.Extensions;
|
||||
|
@ -14,6 +14,7 @@ using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Lightning.CLightning;
|
||||
using BTCPayServer.Models.AccountViewModels;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Payments.PayJoin.Sender;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
@ -22,6 +23,7 @@ using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.CodeAnalysis.Operations;
|
||||
using NBitcoin;
|
||||
using BTCPayServer.BIP78.Sender;
|
||||
using NBitcoin.Payment;
|
||||
using NBitpayClient;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
@ -358,7 +360,7 @@ namespace BTCPayServer.Tests
|
||||
Logs.Tester.LogInformation($"Proposing {psbt.GetGlobalTransaction().GetHash()}");
|
||||
if (expectedError is null && !senderError)
|
||||
{
|
||||
var proposed = await pjClient.RequestPayjoin(endpoint, settings, psbt, default);
|
||||
var proposed = await pjClient.RequestPayjoin(endpoint, new PayjoinWallet(settings), psbt, default);
|
||||
Logs.Tester.LogInformation($"Proposed payjoin is {proposed.GetGlobalTransaction().GetHash()}");
|
||||
Assert.NotNull(proposed);
|
||||
return proposed;
|
||||
@ -367,11 +369,11 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
if (senderError)
|
||||
{
|
||||
await Assert.ThrowsAsync<PayjoinSenderException>(async () => await pjClient.RequestPayjoin(endpoint, settings, psbt, default));
|
||||
await Assert.ThrowsAsync<PayjoinSenderException>(async () => await pjClient.RequestPayjoin(endpoint, new PayjoinWallet(settings), psbt, default));
|
||||
}
|
||||
else
|
||||
{
|
||||
var ex = await Assert.ThrowsAsync<PayjoinReceiverException>(async () => await pjClient.RequestPayjoin(endpoint, settings, psbt, default));
|
||||
var ex = await Assert.ThrowsAsync<PayjoinReceiverException>(async () => await pjClient.RequestPayjoin(endpoint, new PayjoinWallet(settings), psbt, default));
|
||||
var split = expectedError.Split('|');
|
||||
Assert.Equal(split[0], ex.ErrorCode);
|
||||
if (split.Length > 1)
|
||||
|
@ -32,6 +32,7 @@ using BTCPayServer.Models.WalletViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Bitcoin;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Payments.PayJoin.Sender;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Security.Bitpay;
|
||||
using BTCPayServer.Services;
|
||||
@ -1201,7 +1202,7 @@ namespace BTCPayServer.Tests
|
||||
});
|
||||
}
|
||||
var httpFactory = tester.PayTester.GetService<IHttpClientFactory>();
|
||||
var client = httpFactory.CreateClient(PayjoinClient.PayjoinOnionNamedClient);
|
||||
var client = httpFactory.CreateClient(PayjoinServerCommunicator.PayjoinOnionNamedClient);
|
||||
Assert.NotNull(client);
|
||||
var response = await client.GetAsync("https://check.torproject.org/");
|
||||
response.EnsureSuccessStatusCode();
|
||||
@ -1220,7 +1221,7 @@ namespace BTCPayServer.Tests
|
||||
AssertConnectionDropped();
|
||||
client.Dispose();
|
||||
AssertConnectionDropped();
|
||||
client = httpFactory.CreateClient(PayjoinClient.PayjoinOnionNamedClient);
|
||||
client = httpFactory.CreateClient(PayjoinServerCommunicator.PayjoinOnionNamedClient);
|
||||
response = await client.GetAsync("http://explorerzydxu5ecjrkwceayqybizmpjjznk5izmitf2modhcusuqlid.onion/");
|
||||
response.EnsureSuccessStatusCode();
|
||||
AssertConnectionDropped();
|
||||
|
@ -44,6 +44,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BIP78.Sender" Version="0.2.0" />
|
||||
<PackageReference Include="BTCPayServer.Hwi" Version="1.1.3" />
|
||||
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.2.7" />
|
||||
<PackageReference Include="BuildBundlerMinifier" Version="3.2.435" />
|
||||
|
@ -9,9 +9,11 @@ using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.ModelBinders;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.WalletViewModels;
|
||||
using BTCPayServer.Payments.PayJoin.Sender;
|
||||
using BTCPayServer.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin;
|
||||
using BTCPayServer.BIP78.Sender;
|
||||
using NBitcoin.Payment;
|
||||
using NBXplorer;
|
||||
using NBXplorer.Models;
|
||||
@ -171,7 +173,7 @@ namespace BTCPayServer.Controllers
|
||||
var cloned = psbt.Clone();
|
||||
cloned = cloned.Finalize();
|
||||
await _broadcaster.Schedule(DateTimeOffset.UtcNow + TimeSpan.FromMinutes(2.0), cloned.ExtractTransaction(), btcPayNetwork);
|
||||
return await _payjoinClient.RequestPayjoin(bip21, derivationSchemeSettings, psbt, cancellationToken);
|
||||
return await _payjoinClient.RequestPayjoin(bip21, new PayjoinWallet(derivationSchemeSettings), psbt, cancellationToken);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
|
@ -25,6 +25,7 @@ using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin;
|
||||
using BTCPayServer.BIP78.Sender;
|
||||
using NBitcoin.DataEncoders;
|
||||
using NBXplorer;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
|
@ -19,7 +19,6 @@ using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Bitcoin;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
@ -29,11 +28,11 @@ using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin;
|
||||
using BTCPayServer.BIP78.Sender;
|
||||
using NBitcoin.Payment;
|
||||
using NBitpayClient;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using NBXplorer.Models;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer
|
||||
|
@ -313,6 +313,10 @@ namespace BTCPayServer.Hosting
|
||||
services.AddSingleton<IHostedService, HostedServices.CheckConfigurationHostedService>(o => o.GetRequiredService<CheckConfigurationHostedService>());
|
||||
services.AddSingleton<HostedServices.WebhookNotificationManager>();
|
||||
services.AddSingleton<IHostedService, WebhookNotificationManager>(o => o.GetRequiredService<WebhookNotificationManager>());
|
||||
services.AddHttpClient(WebhookNotificationManager.OnionNamedClient)
|
||||
.ConfigureHttpClient(h => h.DefaultRequestHeaders.ConnectionClose = true)
|
||||
.ConfigurePrimaryHttpMessageHandler<Socks5HttpClientHandler>();
|
||||
|
||||
services.AddSingleton<HostedServices.PullPaymentHostedService>();
|
||||
services.AddSingleton<IHostedService, HostedServices.PullPaymentHostedService>(o => o.GetRequiredService<PullPaymentHostedService>());
|
||||
|
||||
|
@ -169,8 +169,8 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||
onchainMethod.DepositAddress = reserved.Address.ToString();
|
||||
onchainMethod.KeyPath = reserved.KeyPath;
|
||||
onchainMethod.PayjoinEnabled = blob.PayJoinEnabled &&
|
||||
PayjoinClient.SupportedFormats.Contains(supportedPaymentMethod
|
||||
.AccountDerivation.ScriptPubKeyType()) &&
|
||||
supportedPaymentMethod
|
||||
.AccountDerivation.ScriptPubKeyType() != ScriptPubKeyType.Legacy &&
|
||||
network.SupportPayJoin;
|
||||
if (onchainMethod.PayjoinEnabled)
|
||||
{
|
||||
|
@ -5,6 +5,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.BIP78.Sender;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Filters;
|
||||
@ -254,7 +255,7 @@ namespace BTCPayServer.Payments.PayJoin
|
||||
continue;
|
||||
|
||||
var receiverInputsType = derivationSchemeSettings.AccountDerivation.ScriptPubKeyType();
|
||||
if (!PayjoinClient.SupportedFormats.Contains(receiverInputsType))
|
||||
if (receiverInputsType == ScriptPubKeyType.Legacy)
|
||||
{
|
||||
//this should never happen, unless the store owner changed the wallet mid way through an invoice
|
||||
return CreatePayjoinErrorAndLog(503, PayjoinReceiverWellknownErrors.Unavailable, "Our wallet does not support payjoin");
|
||||
|
@ -1,7 +1,9 @@
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Payments.PayJoin.Sender;
|
||||
using BTCPayServer.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using BTCPayServer.BIP78.Sender;
|
||||
|
||||
namespace BTCPayServer.Payments.PayJoin
|
||||
{
|
||||
@ -14,12 +16,10 @@ namespace BTCPayServer.Payments.PayJoin
|
||||
services.AddSingleton<HostedServices.Socks5HttpProxyServer>();
|
||||
services.AddSingleton<IHostedService, HostedServices.Socks5HttpProxyServer>(s => s.GetRequiredService<Socks5HttpProxyServer>());
|
||||
services.AddSingleton<PayJoinRepository>();
|
||||
services.AddSingleton<IPayjoinServerCommunicator, PayjoinServerCommunicator>();
|
||||
services.AddSingleton<PayjoinClient>();
|
||||
services.AddTransient<Socks5HttpClientHandler>();
|
||||
services.AddHttpClient(PayjoinClient.PayjoinOnionNamedClient)
|
||||
.ConfigureHttpClient(h => h.DefaultRequestHeaders.ConnectionClose = true)
|
||||
.ConfigurePrimaryHttpMessageHandler<Socks5HttpClientHandler>();
|
||||
services.AddHttpClient(WebhookNotificationManager.OnionNamedClient)
|
||||
services.AddHttpClient(PayjoinServerCommunicator.PayjoinOnionNamedClient)
|
||||
.ConfigureHttpClient(h => h.DefaultRequestHeaders.ConnectionClose = true)
|
||||
.ConfigurePrimaryHttpMessageHandler<Socks5HttpClientHandler>();
|
||||
}
|
||||
|
@ -0,0 +1,23 @@
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using BTCPayServer.BIP78.Sender;
|
||||
|
||||
namespace BTCPayServer.Payments.PayJoin.Sender
|
||||
{
|
||||
public class PayjoinServerCommunicator : HttpClientPayjoinServerCommunicator
|
||||
{
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
public const string PayjoinOnionNamedClient = "payjoin.onion";
|
||||
public const string PayjoinClearnetNamedClient = "payjoin.clearnet";
|
||||
|
||||
public PayjoinServerCommunicator(IHttpClientFactory httpClientFactory)
|
||||
{
|
||||
_httpClientFactory = httpClientFactory;
|
||||
}
|
||||
|
||||
protected override HttpClient CreateHttpClient(Uri uri)
|
||||
{
|
||||
return _httpClientFactory.CreateClient(uri.IsOnion() ? PayjoinOnionNamedClient : PayjoinClearnetNamedClient);
|
||||
}
|
||||
}
|
||||
}
|
32
BTCPayServer/Payments/PayJoin/Sender/PayjoinWallet.cs
Normal file
32
BTCPayServer/Payments/PayJoin/Sender/PayjoinWallet.cs
Normal file
@ -0,0 +1,32 @@
|
||||
using BTCPayServer.BIP78.Sender;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Payments.PayJoin.Sender
|
||||
{
|
||||
public class PayjoinWallet : IPayjoinWallet
|
||||
{
|
||||
private readonly DerivationSchemeSettings _derivationSchemeSettings;
|
||||
|
||||
public PayjoinWallet(DerivationSchemeSettings derivationSchemeSettings)
|
||||
{
|
||||
_derivationSchemeSettings = derivationSchemeSettings;
|
||||
}
|
||||
public IHDScriptPubKey Derive(KeyPath keyPath)
|
||||
{
|
||||
return ((IHDScriptPubKey)_derivationSchemeSettings.AccountDerivation).Derive(keyPath);
|
||||
}
|
||||
|
||||
public bool CanDeriveHardenedPath()
|
||||
{
|
||||
return _derivationSchemeSettings.AccountDerivation.CanDeriveHardenedPath();
|
||||
}
|
||||
|
||||
public Script ScriptPubKey => ((IHDScriptPubKey)_derivationSchemeSettings.AccountDerivation).ScriptPubKey;
|
||||
public ScriptPubKeyType ScriptPubKeyType => _derivationSchemeSettings.AccountDerivation.ScriptPubKeyType();
|
||||
|
||||
public RootedKeyPath RootedKeyPath =>
|
||||
_derivationSchemeSettings.GetSigningAccountKeySettings().GetRootedKeyPath();
|
||||
|
||||
public IHDKey AccountKey => _derivationSchemeSettings.GetSigningAccountKeySettings().AccountKey;
|
||||
}
|
||||
}
|
@ -5,6 +5,7 @@ using BTCPayServer.Payments.Bitcoin;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using NBitcoin;
|
||||
using BTCPayServer.BIP78.Sender;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Payments
|
||||
@ -71,7 +72,7 @@ namespace BTCPayServer.Payments
|
||||
{
|
||||
var bip21 = ((BTCPayNetwork)network).GenerateBIP21(paymentMethodDetails.GetPaymentDestination(), cryptoInfoDue);
|
||||
|
||||
if ((paymentMethodDetails as BitcoinLikeOnChainPaymentMethod)?.PayjoinEnabled is true)
|
||||
if ((paymentMethodDetails as BitcoinLikeOnChainPaymentMethod)?.PayjoinEnabled is true && serverUri != null)
|
||||
{
|
||||
bip21 += $"&{PayjoinClient.BIP21EndpointKey}={serverUri.WithTrailingSlash()}{network.CryptoCode}/{PayjoinClient.BIP21EndpointKey}";
|
||||
}
|
||||
|
@ -1,443 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using NBitcoin;
|
||||
using NBitcoin.Payment;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using IHttpClientFactory = System.Net.Http.IHttpClientFactory;
|
||||
|
||||
namespace BTCPayServer.Services
|
||||
{
|
||||
|
||||
public static class PSBTExtensions
|
||||
{
|
||||
public static ScriptPubKeyType? GetInputsScriptPubKeyType(this PSBT psbt)
|
||||
{
|
||||
if (!psbt.IsAllFinalized())
|
||||
throw new InvalidOperationException("The psbt should be finalized with witness information");
|
||||
var coinsPerTypes = psbt.Inputs.Select(i =>
|
||||
{
|
||||
return ((PSBTCoin)i, i.GetInputScriptPubKeyType());
|
||||
}).GroupBy(o => o.Item2, o => o.Item1).ToArray();
|
||||
if (coinsPerTypes.Length != 1)
|
||||
return default;
|
||||
return coinsPerTypes[0].Key;
|
||||
}
|
||||
|
||||
public static ScriptPubKeyType? GetInputScriptPubKeyType(this PSBTInput i)
|
||||
{
|
||||
var scriptPubKey = i.GetTxOut().ScriptPubKey;
|
||||
if (scriptPubKey.IsScriptType(ScriptType.P2PKH))
|
||||
return ScriptPubKeyType.Legacy;
|
||||
if (scriptPubKey.IsScriptType(ScriptType.P2WPKH))
|
||||
return ScriptPubKeyType.Segwit;
|
||||
if (scriptPubKey.IsScriptType(ScriptType.P2SH) &&
|
||||
i.FinalScriptWitness is WitScript &&
|
||||
PayToWitPubKeyHashTemplate.Instance.ExtractWitScriptParameters(i.FinalScriptWitness) is { })
|
||||
return ScriptPubKeyType.SegwitP2SH;
|
||||
if (scriptPubKey.IsScriptType(ScriptType.P2SH) &&
|
||||
i.RedeemScript is Script &&
|
||||
PayToWitPubKeyHashTemplate.Instance.CheckScriptPubKey(i.RedeemScript))
|
||||
return ScriptPubKeyType.SegwitP2SH;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public class PayjoinClientParameters
|
||||
{
|
||||
public Money MaxAdditionalFeeContribution { get; set; }
|
||||
public FeeRate MinFeeRate { get; set; }
|
||||
public int? AdditionalFeeOutputIndex { get; set; }
|
||||
public bool? DisableOutputSubstitution { get; set; }
|
||||
public int Version { get; set; } = 1;
|
||||
}
|
||||
|
||||
public class PayjoinClient
|
||||
{
|
||||
public const string PayjoinOnionNamedClient = "payjoin.onion";
|
||||
public const string PayjoinClearnetNamedClient = "payjoin.clearnet";
|
||||
public static readonly ScriptPubKeyType[] SupportedFormats = {
|
||||
ScriptPubKeyType.Segwit,
|
||||
ScriptPubKeyType.SegwitP2SH
|
||||
};
|
||||
|
||||
public const string BIP21EndpointKey = "pj";
|
||||
|
||||
private readonly ExplorerClientProvider _explorerClientProvider;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
|
||||
public PayjoinClient(ExplorerClientProvider explorerClientProvider, IHttpClientFactory httpClientFactory)
|
||||
{
|
||||
if (httpClientFactory == null)
|
||||
throw new ArgumentNullException(nameof(httpClientFactory));
|
||||
_explorerClientProvider =
|
||||
explorerClientProvider ?? throw new ArgumentNullException(nameof(explorerClientProvider));
|
||||
_httpClientFactory = httpClientFactory;
|
||||
}
|
||||
|
||||
public Money MaxFeeBumpContribution { get; set; }
|
||||
public FeeRate MinimumFeeRate { get; set; }
|
||||
|
||||
public async Task<PSBT> RequestPayjoin(BitcoinUrlBuilder bip21, DerivationSchemeSettings derivationSchemeSettings,
|
||||
PSBT signedPSBT, CancellationToken cancellationToken)
|
||||
{
|
||||
if (bip21 == null)
|
||||
throw new ArgumentNullException(nameof(bip21));
|
||||
if (!bip21.TryGetPayjoinEndpoint(out var endpoint))
|
||||
throw new InvalidOperationException("This BIP21 does not support payjoin");
|
||||
if (derivationSchemeSettings == null)
|
||||
throw new ArgumentNullException(nameof(derivationSchemeSettings));
|
||||
if (signedPSBT == null)
|
||||
throw new ArgumentNullException(nameof(signedPSBT));
|
||||
if (signedPSBT.IsAllFinalized())
|
||||
throw new InvalidOperationException("The original PSBT should not be finalized.");
|
||||
var optionalParameters = new PayjoinClientParameters();
|
||||
var inputScriptType = derivationSchemeSettings.AccountDerivation.ScriptPubKeyType();
|
||||
var signingAccount = derivationSchemeSettings.GetSigningAccountKeySettings();
|
||||
var paymentScriptPubKey = bip21.Address?.ScriptPubKey;
|
||||
var changeOutput = signedPSBT.Outputs.CoinsFor(derivationSchemeSettings.AccountDerivation, signingAccount.AccountKey, signingAccount.GetRootedKeyPath())
|
||||
.Where(o => o.ScriptPubKey != paymentScriptPubKey)
|
||||
.FirstOrDefault();
|
||||
if (changeOutput is PSBTOutput o)
|
||||
optionalParameters.AdditionalFeeOutputIndex = (int)o.Index;
|
||||
if (!signedPSBT.TryGetEstimatedFeeRate(out var originalFeeRate))
|
||||
throw new ArgumentException("signedPSBT should have utxo information", nameof(signedPSBT));
|
||||
var originalFee = signedPSBT.GetFee();
|
||||
if (changeOutput is PSBTOutput)
|
||||
optionalParameters.MaxAdditionalFeeContribution = MaxFeeBumpContribution is null ?
|
||||
// By default, we want to keep same fee rate and a single additional input
|
||||
originalFeeRate.GetFee(GetVirtualSize(inputScriptType)) :
|
||||
MaxFeeBumpContribution;
|
||||
if (MinimumFeeRate is FeeRate v)
|
||||
optionalParameters.MinFeeRate = v;
|
||||
|
||||
bool allowOutputSubstitution = !(optionalParameters.DisableOutputSubstitution is true);
|
||||
if (bip21.UnknowParameters.TryGetValue("pjos", out var pjos) && pjos == "0")
|
||||
allowOutputSubstitution = false;
|
||||
PSBT originalPSBT = CreateOriginalPSBT(signedPSBT);
|
||||
Transaction originalGlobalTx = signedPSBT.GetGlobalTransaction();
|
||||
TxOut feeOutput = changeOutput == null ? null : originalGlobalTx.Outputs[changeOutput.Index];
|
||||
var originalInputs = new Queue<(TxIn OriginalTxIn, PSBTInput SignedPSBTInput)>();
|
||||
for (int i = 0; i < originalGlobalTx.Inputs.Count; i++)
|
||||
{
|
||||
originalInputs.Enqueue((originalGlobalTx.Inputs[i], signedPSBT.Inputs[i]));
|
||||
}
|
||||
var originalOutputs = new Queue<(TxOut OriginalTxOut, PSBTOutput SignedPSBTOutput)>();
|
||||
for (int i = 0; i < originalGlobalTx.Outputs.Count; i++)
|
||||
{
|
||||
originalOutputs.Enqueue((originalGlobalTx.Outputs[i], signedPSBT.Outputs[i]));
|
||||
}
|
||||
endpoint = ApplyOptionalParameters(endpoint, optionalParameters);
|
||||
var proposal = await SendOriginalTransaction(endpoint, originalPSBT, cancellationToken);
|
||||
// Checking that the PSBT of the receiver is clean
|
||||
if (proposal.GlobalXPubs.Any())
|
||||
{
|
||||
throw new PayjoinSenderException("GlobalXPubs should not be included in the receiver's PSBT");
|
||||
}
|
||||
////////////
|
||||
|
||||
if (proposal.CheckSanity() is List<PSBTError> errors && errors.Count > 0)
|
||||
throw new PayjoinSenderException($"The proposal PSBT is not sane ({errors[0]})");
|
||||
|
||||
var proposalGlobalTx = proposal.GetGlobalTransaction();
|
||||
// Verify that the transaction version, and nLockTime are unchanged.
|
||||
if (proposalGlobalTx.Version != originalGlobalTx.Version)
|
||||
throw new PayjoinSenderException($"The proposal PSBT changed the transaction version");
|
||||
if (proposalGlobalTx.LockTime != originalGlobalTx.LockTime)
|
||||
throw new PayjoinSenderException($"The proposal PSBT changed the nLocktime");
|
||||
|
||||
HashSet<Sequence> sequences = new HashSet<Sequence>();
|
||||
// For each inputs in the proposal:
|
||||
foreach (var proposedPSBTInput in proposal.Inputs)
|
||||
{
|
||||
if (proposedPSBTInput.HDKeyPaths.Count != 0)
|
||||
throw new PayjoinSenderException("The receiver added keypaths to an input");
|
||||
if (proposedPSBTInput.PartialSigs.Count != 0)
|
||||
throw new PayjoinSenderException("The receiver added partial signatures to an input");
|
||||
var proposedTxIn = proposalGlobalTx.Inputs.FindIndexedInput(proposedPSBTInput.PrevOut).TxIn;
|
||||
bool isOurInput = originalInputs.Count > 0 && originalInputs.Peek().OriginalTxIn.PrevOut == proposedPSBTInput.PrevOut;
|
||||
// If it is one of our input
|
||||
if (isOurInput)
|
||||
{
|
||||
var input = originalInputs.Dequeue();
|
||||
// Verify that sequence is unchanged.
|
||||
if (input.OriginalTxIn.Sequence != proposedTxIn.Sequence)
|
||||
throw new PayjoinSenderException("The proposedTxIn modified the sequence of one of our inputs");
|
||||
// Verify the PSBT input is not finalized
|
||||
if (proposedPSBTInput.IsFinalized())
|
||||
throw new PayjoinSenderException("The receiver finalized one of our inputs");
|
||||
// Verify that <code>non_witness_utxo</code> and <code>witness_utxo</code> are not specified.
|
||||
if (proposedPSBTInput.NonWitnessUtxo != null || proposedPSBTInput.WitnessUtxo != null)
|
||||
throw new PayjoinSenderException("The receiver added non_witness_utxo or witness_utxo to one of our inputs");
|
||||
sequences.Add(proposedTxIn.Sequence);
|
||||
|
||||
// Fill up the info from the original PSBT input so we can sign and get fees.
|
||||
proposedPSBTInput.NonWitnessUtxo = input.SignedPSBTInput.NonWitnessUtxo;
|
||||
proposedPSBTInput.WitnessUtxo = input.SignedPSBTInput.WitnessUtxo;
|
||||
// We fill up information we had on the signed PSBT, so we can sign it.
|
||||
foreach (var hdKey in input.SignedPSBTInput.HDKeyPaths)
|
||||
proposedPSBTInput.HDKeyPaths.Add(hdKey.Key, hdKey.Value);
|
||||
proposedPSBTInput.RedeemScript = input.SignedPSBTInput.RedeemScript;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Verify the PSBT input is finalized
|
||||
if (!proposedPSBTInput.IsFinalized())
|
||||
throw new PayjoinSenderException("The receiver did not finalized one of their input");
|
||||
// Verify that non_witness_utxo or witness_utxo are filled in.
|
||||
if (proposedPSBTInput.NonWitnessUtxo == null && proposedPSBTInput.WitnessUtxo == null)
|
||||
throw new PayjoinSenderException("The receiver did not specify non_witness_utxo or witness_utxo for one of their inputs");
|
||||
sequences.Add(proposedTxIn.Sequence);
|
||||
// Verify that the payjoin proposal did not introduced mixed input's type.
|
||||
if (inputScriptType != proposedPSBTInput.GetInputScriptPubKeyType())
|
||||
throw new PayjoinSenderException("Mixed input type detected in the proposal");
|
||||
}
|
||||
}
|
||||
|
||||
// Verify that all of sender's inputs from the original PSBT are in the proposal.
|
||||
if (originalInputs.Count != 0)
|
||||
throw new PayjoinSenderException("Some of our inputs are not included in the proposal");
|
||||
|
||||
// Verify that the payjoin proposal did not introduced mixed input's sequence.
|
||||
if (sequences.Count != 1)
|
||||
throw new PayjoinSenderException("Mixed sequence detected in the proposal");
|
||||
|
||||
if (!proposal.TryGetFee(out var newFee))
|
||||
throw new PayjoinSenderException("The payjoin receiver did not included UTXO information to calculate fee correctly");
|
||||
var additionalFee = newFee - originalFee;
|
||||
if (additionalFee < Money.Zero)
|
||||
throw new PayjoinSenderException("The receiver decreased absolute fee");
|
||||
|
||||
// For each outputs in the proposal:
|
||||
foreach (var proposedPSBTOutput in proposal.Outputs)
|
||||
{
|
||||
// Verify that no keypaths is in the PSBT output
|
||||
if (proposedPSBTOutput.HDKeyPaths.Count != 0)
|
||||
throw new PayjoinSenderException("The receiver added keypaths to an output");
|
||||
bool isOriginalOutput = originalOutputs.Count > 0 && originalOutputs.Peek().OriginalTxOut.ScriptPubKey == proposedPSBTOutput.ScriptPubKey;
|
||||
if (isOriginalOutput)
|
||||
{
|
||||
var originalOutput = originalOutputs.Dequeue();
|
||||
if (originalOutput.OriginalTxOut == feeOutput)
|
||||
{
|
||||
var actualContribution = feeOutput.Value - proposedPSBTOutput.Value;
|
||||
// The amount that was substracted from the output's value is less or equal to maxadditionalfeecontribution
|
||||
if (actualContribution > optionalParameters.MaxAdditionalFeeContribution)
|
||||
throw new PayjoinSenderException("The actual contribution is more than maxadditionalfeecontribution");
|
||||
// Make sure the actual contribution is only paying fee
|
||||
if (actualContribution > additionalFee)
|
||||
throw new PayjoinSenderException("The actual contribution is not only paying fee");
|
||||
// Make sure the actual contribution is only paying for fee incurred by additional inputs
|
||||
var additionalInputsCount = proposalGlobalTx.Inputs.Count - originalGlobalTx.Inputs.Count;
|
||||
if (actualContribution > originalFeeRate.GetFee(GetVirtualSize(inputScriptType)) * additionalInputsCount)
|
||||
throw new PayjoinSenderException("The actual contribution is not only paying for additional inputs");
|
||||
}
|
||||
else if (allowOutputSubstitution &&
|
||||
originalOutput.OriginalTxOut.ScriptPubKey == paymentScriptPubKey)
|
||||
{
|
||||
// That's the payment output, the receiver may have changed it.
|
||||
}
|
||||
else
|
||||
{
|
||||
if (originalOutput.OriginalTxOut.Value > proposedPSBTOutput.Value)
|
||||
throw new PayjoinSenderException("The receiver decreased the value of one of the outputs");
|
||||
}
|
||||
// We fill up information we had on the signed PSBT, so we can sign it.
|
||||
foreach (var hdKey in originalOutput.SignedPSBTOutput.HDKeyPaths)
|
||||
proposedPSBTOutput.HDKeyPaths.Add(hdKey.Key, hdKey.Value);
|
||||
proposedPSBTOutput.RedeemScript = originalOutput.SignedPSBTOutput.RedeemScript;
|
||||
}
|
||||
}
|
||||
// Verify that all of sender's outputs from the original PSBT are in the proposal.
|
||||
if (originalOutputs.Count != 0)
|
||||
{
|
||||
if (!allowOutputSubstitution ||
|
||||
originalOutputs.Count != 1 ||
|
||||
originalOutputs.Dequeue().OriginalTxOut.ScriptPubKey != paymentScriptPubKey)
|
||||
{
|
||||
throw new PayjoinSenderException("Some of our outputs are not included in the proposal");
|
||||
}
|
||||
}
|
||||
|
||||
// If minfeerate was specified, check that the fee rate of the payjoin transaction is not less than this value.
|
||||
if (optionalParameters.MinFeeRate is FeeRate minFeeRate)
|
||||
{
|
||||
if (!proposal.TryGetEstimatedFeeRate(out var newFeeRate))
|
||||
throw new PayjoinSenderException("The payjoin receiver did not included UTXO information to calculate fee correctly");
|
||||
if (newFeeRate < minFeeRate)
|
||||
throw new PayjoinSenderException("The payjoin receiver created a payjoin with a too low fee rate");
|
||||
}
|
||||
return proposal;
|
||||
}
|
||||
|
||||
private int GetVirtualSize(ScriptPubKeyType? scriptPubKeyType)
|
||||
{
|
||||
switch (scriptPubKeyType)
|
||||
{
|
||||
case ScriptPubKeyType.Legacy:
|
||||
return 148;
|
||||
case ScriptPubKeyType.Segwit:
|
||||
return 68;
|
||||
case ScriptPubKeyType.SegwitP2SH:
|
||||
return 91;
|
||||
default:
|
||||
return 110;
|
||||
}
|
||||
}
|
||||
|
||||
private static PSBT CreateOriginalPSBT(PSBT signedPSBT)
|
||||
{
|
||||
var original = signedPSBT.Clone();
|
||||
original = original.Finalize();
|
||||
foreach (var input in original.Inputs)
|
||||
{
|
||||
input.HDKeyPaths.Clear();
|
||||
input.PartialSigs.Clear();
|
||||
input.Unknown.Clear();
|
||||
}
|
||||
foreach (var output in original.Outputs)
|
||||
{
|
||||
output.Unknown.Clear();
|
||||
output.HDKeyPaths.Clear();
|
||||
}
|
||||
original.GlobalXPubs.Clear();
|
||||
return original;
|
||||
}
|
||||
|
||||
private async Task<PSBT> SendOriginalTransaction(Uri endpoint, PSBT originalTx, CancellationToken cancellationToken)
|
||||
{
|
||||
using (HttpClient client = CreateHttpClient(endpoint))
|
||||
{
|
||||
var bpuresponse = await client.PostAsync(endpoint,
|
||||
new StringContent(originalTx.ToBase64(), Encoding.UTF8, "text/plain"), cancellationToken);
|
||||
if (!bpuresponse.IsSuccessStatusCode)
|
||||
{
|
||||
var errorStr = await bpuresponse.Content.ReadAsStringAsync();
|
||||
try
|
||||
{
|
||||
var error = JObject.Parse(errorStr);
|
||||
throw new PayjoinReceiverException(error["errorCode"].Value<string>(),
|
||||
error["message"].Value<string>());
|
||||
}
|
||||
catch (JsonReaderException)
|
||||
{
|
||||
// will throw
|
||||
bpuresponse.EnsureSuccessStatusCode();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
var hex = await bpuresponse.Content.ReadAsStringAsync();
|
||||
return PSBT.Parse(hex, originalTx.Network);
|
||||
}
|
||||
}
|
||||
|
||||
private static Uri ApplyOptionalParameters(Uri endpoint, PayjoinClientParameters clientParameters)
|
||||
{
|
||||
var requestUri = endpoint.AbsoluteUri;
|
||||
if (requestUri.IndexOf('?', StringComparison.OrdinalIgnoreCase) is int i && i != -1)
|
||||
requestUri = requestUri.Substring(0, i);
|
||||
List<string> parameters = new List<string>(3);
|
||||
parameters.Add($"v={clientParameters.Version}");
|
||||
if (clientParameters.AdditionalFeeOutputIndex is int additionalFeeOutputIndex)
|
||||
parameters.Add($"additionalfeeoutputindex={additionalFeeOutputIndex.ToString(CultureInfo.InvariantCulture)}");
|
||||
if (clientParameters.DisableOutputSubstitution is bool disableoutputsubstitution)
|
||||
parameters.Add($"disableoutputsubstitution={disableoutputsubstitution}");
|
||||
if (clientParameters.MaxAdditionalFeeContribution is Money maxAdditionalFeeContribution)
|
||||
parameters.Add($"maxadditionalfeecontribution={maxAdditionalFeeContribution.Satoshi.ToString(CultureInfo.InvariantCulture)}");
|
||||
if (clientParameters.MinFeeRate is FeeRate minFeeRate)
|
||||
parameters.Add($"minfeerate={minFeeRate.SatoshiPerByte.ToString(CultureInfo.InvariantCulture)}");
|
||||
endpoint = new Uri($"{requestUri}?{string.Join('&', parameters)}");
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
private HttpClient CreateHttpClient(Uri uri)
|
||||
{
|
||||
if (uri.IsOnion())
|
||||
return _httpClientFactory.CreateClient(PayjoinOnionNamedClient);
|
||||
else
|
||||
return _httpClientFactory.CreateClient(PayjoinClearnetNamedClient);
|
||||
}
|
||||
}
|
||||
|
||||
public class PayjoinException : Exception
|
||||
{
|
||||
public PayjoinException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public enum PayjoinReceiverWellknownErrors
|
||||
{
|
||||
Unavailable,
|
||||
NotEnoughMoney,
|
||||
VersionUnsupported,
|
||||
OriginalPSBTRejected
|
||||
}
|
||||
public class PayjoinReceiverHelper
|
||||
{
|
||||
static IEnumerable<(PayjoinReceiverWellknownErrors EnumValue, string ErrorCode, string Message)> Get()
|
||||
{
|
||||
yield return (PayjoinReceiverWellknownErrors.Unavailable, "unavailable", "The payjoin endpoint is not available for now.");
|
||||
yield return (PayjoinReceiverWellknownErrors.NotEnoughMoney, "not-enough-money", "The receiver added some inputs but could not bump the fee of the payjoin proposal.");
|
||||
yield return (PayjoinReceiverWellknownErrors.VersionUnsupported, "version-unsupported", "This version of payjoin is not supported.");
|
||||
yield return (PayjoinReceiverWellknownErrors.OriginalPSBTRejected, "original-psbt-rejected", "The receiver rejected the original PSBT.");
|
||||
}
|
||||
public static string GetErrorCode(PayjoinReceiverWellknownErrors err)
|
||||
{
|
||||
return Get().Single(o => o.EnumValue == err).ErrorCode;
|
||||
}
|
||||
public static PayjoinReceiverWellknownErrors? GetWellknownError(string errorCode)
|
||||
{
|
||||
var t = Get().FirstOrDefault(o => o.ErrorCode == errorCode);
|
||||
if (t == default)
|
||||
return null;
|
||||
return t.EnumValue;
|
||||
}
|
||||
static readonly string UnknownError = "Unknown error from the receiver";
|
||||
public static string GetMessage(string errorCode)
|
||||
{
|
||||
return Get().FirstOrDefault(o => o.ErrorCode == errorCode).Message ?? UnknownError;
|
||||
}
|
||||
public static string GetMessage(PayjoinReceiverWellknownErrors err)
|
||||
{
|
||||
return Get().Single(o => o.EnumValue == err).Message;
|
||||
}
|
||||
}
|
||||
public class PayjoinReceiverException : PayjoinException
|
||||
{
|
||||
public PayjoinReceiverException(string errorCode, string receiverMessage) : base(FormatMessage(errorCode, receiverMessage))
|
||||
{
|
||||
ErrorCode = errorCode;
|
||||
ReceiverMessage = receiverMessage;
|
||||
WellknownError = PayjoinReceiverHelper.GetWellknownError(errorCode);
|
||||
ErrorMessage = PayjoinReceiverHelper.GetMessage(errorCode);
|
||||
}
|
||||
public string ErrorCode { get; }
|
||||
public string ErrorMessage { get; }
|
||||
public string ReceiverMessage { get; }
|
||||
|
||||
public PayjoinReceiverWellknownErrors? WellknownError
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
private static string FormatMessage(string errorCode, string receiverMessage)
|
||||
{
|
||||
return $"{errorCode}: {PayjoinReceiverHelper.GetMessage(errorCode)}. (Receiver message: {receiverMessage})";
|
||||
}
|
||||
}
|
||||
|
||||
public class PayjoinSenderException : PayjoinException
|
||||
{
|
||||
public PayjoinSenderException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
@using BTCPayServer.Services
|
||||
@using BTCPayServer.BIP78.Sender
|
||||
@model BTCPayServer.Models.InvoicingModels.PaymentModel
|
||||
|
||||
<script type="text/x-template" id="bitcoin-method-checkout-template">
|
||||
|
Loading…
Reference in New Issue
Block a user