mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-02-23 14:40:36 +01:00
commit
111feeb673
6 changed files with 160 additions and 38 deletions
|
@ -1,4 +1,5 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
|
@ -40,6 +41,64 @@ namespace BTCPayServer.Tests
|
|||
Logs.LogProvider = new XUnitLogProvider(helper);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanOnlyUseCorrectAddressFormatsForPayjoin()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
await tester.StartAsync();
|
||||
var broadcaster = tester.PayTester.GetService<DelayedTransactionBroadcaster>();
|
||||
var payjoinRepository = tester.PayTester.GetService<PayJoinRepository>();
|
||||
broadcaster.Disable();
|
||||
var network = tester.NetworkProvider.GetNetwork<BTCPayNetwork>("BTC");
|
||||
var btcPayWallet = tester.PayTester.GetService<BTCPayWalletProvider>().GetWallet(network);
|
||||
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));
|
||||
|
||||
|
||||
foreach (ScriptPubKeyType senderAddressType in Enum.GetValues(typeof(ScriptPubKeyType)))
|
||||
{
|
||||
var senderUser = tester.NewAccount();
|
||||
senderUser.GrantAccess(true);
|
||||
senderUser.RegisterDerivationScheme("BTC", senderAddressType);
|
||||
|
||||
foreach (ScriptPubKeyType receiverAddressType in Enum.GetValues(typeof(ScriptPubKeyType)))
|
||||
{
|
||||
var senderCoin = await senderUser.ReceiveUTXO(Money.Satoshis(100000), network);
|
||||
|
||||
Logs.Tester.LogInformation($"Testing payjoin with sender: {senderAddressType} receiver: {receiverAddressType}");
|
||||
var receiverUser = tester.NewAccount();
|
||||
receiverUser.GrantAccess(true);
|
||||
receiverUser.RegisterDerivationScheme("BTC", receiverAddressType, true);
|
||||
await receiverUser.EnablePayJoin();
|
||||
var receiverCoin = await receiverUser.ReceiveUTXO(Money.Satoshis(810), network);
|
||||
|
||||
var clientShouldError = unsupportedFormats.Contains(senderAddressType);
|
||||
var errorCode = ( unsupportedFormats.Contains( receiverAddressType) || receiverAddressType != senderAddressType)? "unsupported-inputs" : null;
|
||||
|
||||
var invoice = receiverUser.BitPay.CreateInvoice(new Invoice() {Price = 50000, Currency = "sats", FullNotifications = true});
|
||||
|
||||
var invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network);
|
||||
var txBuilder = network.NBitcoinNetwork.CreateTransactionBuilder();
|
||||
|
||||
txBuilder.AddCoins(senderCoin);
|
||||
txBuilder.Send(invoiceAddress, invoice.BtcDue);
|
||||
txBuilder.SetChange(await senderUser.GetNewAddress(network));
|
||||
txBuilder.SendEstimatedFees(new FeeRate(50m));
|
||||
var psbt = txBuilder.BuildPSBT(false);
|
||||
psbt = await senderUser.Sign(psbt);
|
||||
var pj = await senderUser.SubmitPayjoin(invoice, psbt, errorCode, clientShouldError);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Selenium", "Selenium")]
|
||||
public async Task CanUseBIP79Client()
|
||||
|
@ -60,7 +119,7 @@ namespace BTCPayServer.Tests
|
|||
s.GoToInvoiceCheckout(invoiceId);
|
||||
var bip21 = s.Driver.FindElement(By.ClassName("payment__details__instruction__open-wallet__btn"))
|
||||
.GetAttribute("href");
|
||||
Assert.DoesNotContain("bpu=", bip21);
|
||||
Assert.DoesNotContain($"{PayjoinClient.BIP21EndpointKey}=", bip21);
|
||||
|
||||
s.GoToHome();
|
||||
s.GoToStore(receiver.storeId);
|
||||
|
@ -79,7 +138,7 @@ namespace BTCPayServer.Tests
|
|||
s.GoToInvoiceCheckout(invoiceId);
|
||||
bip21 = s.Driver.FindElement(By.ClassName("payment__details__instruction__open-wallet__btn"))
|
||||
.GetAttribute("href");
|
||||
Assert.Contains("bpu=", bip21);
|
||||
Assert.Contains($"{PayjoinClient.BIP21EndpointKey}=", bip21);
|
||||
|
||||
s.GoToWalletSend(senderWalletId);
|
||||
s.Driver.FindElement(By.Id("bip21parse")).Click();
|
||||
|
@ -111,7 +170,7 @@ namespace BTCPayServer.Tests
|
|||
s.GoToInvoiceCheckout(invoiceId);
|
||||
bip21 = s.Driver.FindElement(By.ClassName("payment__details__instruction__open-wallet__btn"))
|
||||
.GetAttribute("href");
|
||||
Assert.Contains("bpu", bip21);
|
||||
Assert.Contains($"{PayjoinClient.BIP21EndpointKey}", bip21);
|
||||
|
||||
s.GoToWalletSend(senderWalletId);
|
||||
s.Driver.FindElement(By.Id("bip21parse")).Click();
|
||||
|
@ -179,11 +238,11 @@ namespace BTCPayServer.Tests
|
|||
|
||||
var senderUser = tester.NewAccount();
|
||||
senderUser.GrantAccess(true);
|
||||
senderUser.RegisterDerivationScheme("BTC", true);
|
||||
senderUser.RegisterDerivationScheme("BTC", ScriptPubKeyType.Segwit);
|
||||
|
||||
var receiverUser = tester.NewAccount();
|
||||
receiverUser.GrantAccess(true);
|
||||
receiverUser.RegisterDerivationScheme("BTC", true, true);
|
||||
receiverUser.RegisterDerivationScheme("BTC", ScriptPubKeyType.Segwit, true);
|
||||
await receiverUser.EnablePayJoin();
|
||||
var receiverCoin = await receiverUser.ReceiveUTXO(Money.Satoshis(810), network);
|
||||
string lastInvoiceId = null;
|
||||
|
@ -330,9 +389,8 @@ namespace BTCPayServer.Tests
|
|||
Assert.True(result.Success);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
// [Fact(Timeout = TestTimeout)]
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanUseBIP79()
|
||||
{
|
||||
|
@ -348,18 +406,18 @@ namespace BTCPayServer.Tests
|
|||
|
||||
var senderUser = tester.NewAccount();
|
||||
senderUser.GrantAccess(true);
|
||||
senderUser.RegisterDerivationScheme("BTC", true, true);
|
||||
senderUser.RegisterDerivationScheme("BTC", ScriptPubKeyType.Segwit, true);
|
||||
|
||||
var invoice = senderUser.BitPay.CreateInvoice(
|
||||
new Invoice() {Price = 100, Currency = "USD", FullNotifications = true});
|
||||
//payjoin is not enabled by default.
|
||||
Assert.DoesNotContain("bpu", invoice.CryptoInfo.First().PaymentUrls.BIP21);
|
||||
Assert.DoesNotContain($"{PayjoinClient.BIP21EndpointKey}", invoice.CryptoInfo.First().PaymentUrls.BIP21);
|
||||
cashCow.SendToAddress(BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network),
|
||||
Money.Coins(0.06m));
|
||||
|
||||
var receiverUser = tester.NewAccount();
|
||||
receiverUser.GrantAccess(true);
|
||||
receiverUser.RegisterDerivationScheme("BTC", true, true);
|
||||
receiverUser.RegisterDerivationScheme("BTC", ScriptPubKeyType.Segwit, true);
|
||||
|
||||
await receiverUser.EnablePayJoin();
|
||||
// payjoin is enabled, with a segwit wallet, and the keys are available in nbxplorer
|
||||
|
|
|
@ -19,6 +19,7 @@ using System.Threading.Tasks;
|
|||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Lightning.CLightning;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Views.Manage;
|
||||
using BTCPayServer.Views.Stores;
|
||||
using Newtonsoft.Json;
|
||||
|
@ -332,7 +333,7 @@ namespace BTCPayServer.Tests
|
|||
GoToInvoiceCheckout(invoiceId);
|
||||
var bip21 = Driver.FindElement(By.ClassName("payment__details__instruction__open-wallet__btn"))
|
||||
.GetAttribute("href");
|
||||
Assert.Contains("bpu", bip21);
|
||||
Assert.Contains($"{PayjoinClient.BIP21EndpointKey}", bip21);
|
||||
|
||||
GoToWalletSend(walletId);
|
||||
Driver.FindElement(By.Id("bip21parse")).Click();
|
||||
|
|
|
@ -23,6 +23,7 @@ using BTCPayServer.Data;
|
|||
using Microsoft.AspNetCore.Identity;
|
||||
using NBXplorer.Models;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
|
@ -141,19 +142,19 @@ namespace BTCPayServer.Tests
|
|||
|
||||
public BTCPayNetwork SupportedNetwork { get; set; }
|
||||
|
||||
public WalletId RegisterDerivationScheme(string crytoCode, bool segwit = false, bool importKeysToNBX = false)
|
||||
public WalletId RegisterDerivationScheme(string crytoCode, ScriptPubKeyType segwit = ScriptPubKeyType.Legacy, bool importKeysToNBX = false)
|
||||
{
|
||||
return RegisterDerivationSchemeAsync(crytoCode, segwit, importKeysToNBX).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public async Task<WalletId> RegisterDerivationSchemeAsync(string cryptoCode, bool segwit = false,
|
||||
public async Task<WalletId> RegisterDerivationSchemeAsync(string cryptoCode, ScriptPubKeyType segwit = ScriptPubKeyType.Legacy,
|
||||
bool importKeysToNBX = false)
|
||||
{
|
||||
SupportedNetwork = parent.NetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
|
||||
var store = parent.PayTester.GetController<StoresController>(UserId, StoreId);
|
||||
GenerateWalletResponseV = await parent.ExplorerClient.GenerateWalletAsync(new GenerateWalletRequest()
|
||||
{
|
||||
ScriptPubKeyType = segwit ? ScriptPubKeyType.Segwit : ScriptPubKeyType.Legacy,
|
||||
ScriptPubKeyType = segwit,
|
||||
SavePrivateKeys = importKeysToNBX,
|
||||
});
|
||||
await store.AddDerivationScheme(StoreId,
|
||||
|
@ -266,9 +267,25 @@ namespace BTCPayServer.Tests
|
|||
var cashCow = parent.ExplorerNode;
|
||||
var btcPayWallet = parent.PayTester.GetService<BTCPayWalletProvider>().GetWallet(network);
|
||||
var address = (await btcPayWallet.ReserveAddressAsync(this.DerivationScheme)).Address;
|
||||
var txid = await cashCow.SendToAddressAsync(address, value);
|
||||
var tx = await cashCow.GetRawTransactionAsync(txid);
|
||||
return tx.Outputs.AsCoins().First(c => c.ScriptPubKey == address.ScriptPubKey);
|
||||
await parent.WaitForEvent<NewOnChainTransactionEvent>(async () =>
|
||||
{
|
||||
await cashCow.SendToAddressAsync(address, value);
|
||||
});
|
||||
int i = 0;
|
||||
while (i <30)
|
||||
{
|
||||
var result = (await btcPayWallet.GetUnspentCoins(DerivationScheme))
|
||||
.FirstOrDefault(c => c.ScriptPubKey == address.ScriptPubKey)?.Coin;
|
||||
if (result != null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
await Task.Delay(1000);
|
||||
i++;
|
||||
}
|
||||
Assert.False(true);
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task<BitcoinAddress> GetNewAddress(BTCPayNetwork network)
|
||||
|
@ -293,16 +310,20 @@ namespace BTCPayServer.Tests
|
|||
GenerateWalletResponseV.AccountKeyPath);
|
||||
}
|
||||
|
||||
public async Task<PSBT> SubmitPayjoin(Invoice invoice, PSBT psbt, string expectedError = null)
|
||||
public async Task<PSBT> SubmitPayjoin(Invoice invoice, PSBT psbt, string expectedError = null, bool senderError= false)
|
||||
{
|
||||
var endpoint = GetPayjoinEndpoint(invoice, psbt.Network);
|
||||
if (endpoint == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var pjClient = parent.PayTester.GetService<PayjoinClient>();
|
||||
var storeRepository = parent.PayTester.GetService<StoreRepository>();
|
||||
var store = await storeRepository.FindStore(StoreId);
|
||||
var settings = store.GetSupportedPaymentMethods(parent.NetworkProvider).OfType<DerivationSchemeSettings>()
|
||||
.First();
|
||||
Logs.Tester.LogInformation($"Proposing {psbt.GetGlobalTransaction().GetHash()}");
|
||||
if (expectedError is null)
|
||||
if (expectedError is null && !senderError)
|
||||
{
|
||||
var proposed = await pjClient.RequestPayjoin(endpoint, settings, psbt, default);
|
||||
Logs.Tester.LogInformation($"Proposed payjoin is {proposed.GetGlobalTransaction().GetHash()}");
|
||||
|
@ -311,8 +332,15 @@ namespace BTCPayServer.Tests
|
|||
}
|
||||
else
|
||||
{
|
||||
var ex = await Assert.ThrowsAsync<PayjoinReceiverException>(async () => await pjClient.RequestPayjoin(endpoint, settings, psbt, default));
|
||||
Assert.Equal(expectedError, ex.ErrorCode);
|
||||
if (senderError)
|
||||
{
|
||||
await Assert.ThrowsAsync<PayjoinSenderException>(async () => await pjClient.RequestPayjoin(endpoint, settings, psbt, default));
|
||||
}
|
||||
else
|
||||
{
|
||||
var ex = await Assert.ThrowsAsync<PayjoinReceiverException>(async () => await pjClient.RequestPayjoin(endpoint, settings, psbt, default));
|
||||
Assert.Equal(expectedError, ex.ErrorCode);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -359,7 +387,7 @@ namespace BTCPayServer.Tests
|
|||
var parsedBip21 = new BitcoinUrlBuilder(
|
||||
invoice.CryptoInfo.First(c => c.CryptoCode == network.NetworkSet.CryptoCode).PaymentUrls.BIP21,
|
||||
network);
|
||||
return new Uri(parsedBip21.UnknowParameters["bpu"], UriKind.Absolute);
|
||||
return parsedBip21.UnknowParameters.TryGetValue($"{PayjoinClient.BIP21EndpointKey}", out var uri) ? new Uri(uri, UriKind.Absolute) : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -897,7 +897,7 @@ namespace BTCPayServer.Tests
|
|||
await tester.StartAsync();
|
||||
var acc = tester.NewAccount();
|
||||
acc.GrantAccess();
|
||||
acc.RegisterDerivationScheme("BTC", true);
|
||||
acc.RegisterDerivationScheme("BTC", ScriptPubKeyType.Segwit);
|
||||
var btcDerivationScheme = acc.DerivationScheme;
|
||||
|
||||
var walletController = acc.GetController<WalletsController>();
|
||||
|
|
|
@ -4,7 +4,6 @@ using System.IO;
|
|||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.HostedServices;
|
||||
|
@ -14,12 +13,8 @@ using BTCPayServer.Services.Invoices;
|
|||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using NBitcoin.Logging;
|
||||
using NBXplorer;
|
||||
using NBXplorer.Models;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
@ -30,7 +25,7 @@ using System.Diagnostics.CodeAnalysis;
|
|||
|
||||
namespace BTCPayServer.Payments.PayJoin
|
||||
{
|
||||
[Route("{cryptoCode}/bpu")]
|
||||
[Route("{cryptoCode}/" + PayjoinClient.BIP21EndpointKey)]
|
||||
public class PayJoinEndpointController : ControllerBase
|
||||
{
|
||||
/// <summary>
|
||||
|
@ -172,9 +167,11 @@ namespace BTCPayServer.Payments.PayJoin
|
|||
{
|
||||
await _explorerClientProvider.GetExplorerClient(network).BroadcastAsync(originalTx);
|
||||
}
|
||||
|
||||
if (originalTx.Inputs.Any(i => !(i.GetSigner() is WitKeyId)))
|
||||
return BadRequest(CreatePayjoinError(400, "unsupported-inputs", "Payjoin only support P2WPKH inputs"));
|
||||
|
||||
var allNativeSegwit = psbt.Inputs.All(i => i.ScriptPubKeyType() == ScriptPubKeyType.Segwit);
|
||||
var allScript = psbt.Inputs.All(i => i.ScriptPubKeyType() == ScriptPubKeyType.SegwitP2SH);
|
||||
if (!allNativeSegwit && !allScript)
|
||||
return BadRequest(CreatePayjoinError(400, "unsupported-inputs", "Payjoin only support segwit inputs (of the same type)"));
|
||||
if (psbt.CheckSanity() is var errors && errors.Count != 0)
|
||||
{
|
||||
return BadRequest(CreatePayjoinError(400, "insane-psbt", $"This PSBT is insane ({errors[0]})"));
|
||||
|
@ -235,6 +232,17 @@ namespace BTCPayServer.Payments.PayJoin
|
|||
.SingleOrDefault();
|
||||
if (derivationSchemeSettings is null)
|
||||
continue;
|
||||
|
||||
var type = derivationSchemeSettings.AccountDerivation.ScriptPubKeyType();
|
||||
if (!PayjoinClient.SupportedFormats.Contains(type))
|
||||
{
|
||||
//this should never happen, unless the store owner changed the wallet mid way through an invoice
|
||||
return StatusCode(500, CreatePayjoinError(500, "unavailable", $"This service is unavailable for now"));
|
||||
}
|
||||
else if ((type == ScriptPubKeyType.Segwit && !allNativeSegwit) ||
|
||||
(type == ScriptPubKeyType.SegwitP2SH && allScript))
|
||||
return BadRequest(CreatePayjoinError(400, "unsupported-inputs",
|
||||
"Payjoin only support segwit inputs (of the same type)"));
|
||||
var paymentMethod = invoice.GetPaymentMethod(paymentMethodId);
|
||||
var paymentDetails =
|
||||
paymentMethod.GetPaymentMethodDetails() as Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod;
|
||||
|
|
|
@ -2,19 +2,38 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Google.Apis.Util;
|
||||
using NBitcoin;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Services
|
||||
{
|
||||
|
||||
public static class PSBTExtensions
|
||||
{
|
||||
public static ScriptPubKeyType? ScriptPubKeyType(this PSBTInput i)
|
||||
{
|
||||
if (i.WitnessUtxo.ScriptPubKey.IsScriptType(ScriptType.P2WPKH))
|
||||
return NBitcoin.ScriptPubKeyType.Segwit;
|
||||
if (i.WitnessUtxo.ScriptPubKey.IsScriptType(ScriptType.P2SH) &&
|
||||
i.FinalScriptWitness.ToScript().IsScriptType(ScriptType.P2WPKH))
|
||||
return NBitcoin.ScriptPubKeyType.SegwitP2SH;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public class PayjoinClient
|
||||
{
|
||||
public static readonly ScriptPubKeyType[] SupportedFormats = {
|
||||
ScriptPubKeyType.Segwit,
|
||||
ScriptPubKeyType.SegwitP2SH
|
||||
};
|
||||
|
||||
public const string BIP21EndpointKey = "bpu";
|
||||
|
||||
private readonly ExplorerClientProvider _explorerClientProvider;
|
||||
private HttpClient _httpClient;
|
||||
|
||||
|
@ -35,6 +54,11 @@ namespace BTCPayServer.Services
|
|||
if (originalTx.IsAllFinalized())
|
||||
throw new InvalidOperationException("The original PSBT should not be finalized.");
|
||||
|
||||
var type = derivationSchemeSettings.AccountDerivation.ScriptPubKeyType();
|
||||
if (!SupportedFormats.Contains(type))
|
||||
{
|
||||
throw new PayjoinSenderException($"The wallet does not support payjoin");
|
||||
}
|
||||
var signingAccount = derivationSchemeSettings.GetSigningAccountKeySettings();
|
||||
var sentBefore = -originalTx.GetBalance(derivationSchemeSettings.AccountDerivation,
|
||||
signingAccount.AccountKey,
|
||||
|
@ -141,15 +165,18 @@ namespace BTCPayServer.Services
|
|||
}
|
||||
}
|
||||
|
||||
// Making sure that the receiver's inputs are finalized and P2PWKH
|
||||
// Making sure that the receiver's inputs are finalized and match format
|
||||
foreach (var input in newPSBT.Inputs)
|
||||
{
|
||||
if (originalTx.Inputs.FindIndexedInput(input.PrevOut) is null)
|
||||
{
|
||||
if (!input.IsFinalized())
|
||||
throw new PayjoinSenderException("The payjoin receiver included a non finalized input");
|
||||
if (!(input.FinalScriptWitness.GetSigner() is WitKeyId))
|
||||
throw new PayjoinSenderException("The payjoin receiver included an input that is not P2PWKH");
|
||||
|
||||
if (type != input.ScriptPubKeyType())
|
||||
{
|
||||
throw new PayjoinSenderException("The payjoin receiver included an input that is not the same segwit input type");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue