mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-17 22:14:26 +01:00
Adapt payjoin implementation to the BIP (#1569)
This commit is contained in:
74
BTCPayServer.Tests/FakeServer.cs
Normal file
74
BTCPayServer.Tests/FakeServer.cs
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
using System.Threading.Channels;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.AspNetCore.Hosting.Server.Features;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Tests
|
||||||
|
{
|
||||||
|
public class FakeServer : IDisposable
|
||||||
|
{
|
||||||
|
IWebHost webHost;
|
||||||
|
SemaphoreSlim semaphore;
|
||||||
|
CancellationTokenSource cts = new CancellationTokenSource();
|
||||||
|
public FakeServer()
|
||||||
|
{
|
||||||
|
_channel = Channel.CreateUnbounded<HttpContext>();
|
||||||
|
semaphore = new SemaphoreSlim(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
Channel<HttpContext> _channel;
|
||||||
|
public async Task Start()
|
||||||
|
{
|
||||||
|
webHost = new WebHostBuilder()
|
||||||
|
.UseKestrel()
|
||||||
|
.UseUrls("http://127.0.0.1:0")
|
||||||
|
.Configure(appBuilder =>
|
||||||
|
{
|
||||||
|
appBuilder.Run(async ctx =>
|
||||||
|
{
|
||||||
|
await _channel.Writer.WriteAsync(ctx);
|
||||||
|
await semaphore.WaitAsync(cts.Token);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.Build();
|
||||||
|
await webHost.StartAsync();
|
||||||
|
var port = new Uri(webHost.ServerFeatures.Get<IServerAddressesFeature>().Addresses.First(), UriKind.Absolute)
|
||||||
|
.Port;
|
||||||
|
ServerUri = new Uri($"http://127.0.0.1:{port}/");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Uri ServerUri { get; set; }
|
||||||
|
|
||||||
|
public void Done()
|
||||||
|
{
|
||||||
|
semaphore.Release();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Stop()
|
||||||
|
{
|
||||||
|
await webHost.StopAsync();
|
||||||
|
}
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
cts.Dispose();
|
||||||
|
webHost?.Dispose();
|
||||||
|
semaphore.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<HttpContext> GetNextRequest()
|
||||||
|
{
|
||||||
|
return await _channel.Reader.ReadAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ using BTCPayServer.Services.Invoices;
|
|||||||
using BTCPayServer.Services.Wallets;
|
using BTCPayServer.Services.Wallets;
|
||||||
using BTCPayServer.Tests.Logging;
|
using BTCPayServer.Tests.Logging;
|
||||||
using BTCPayServer.Views.Wallets;
|
using BTCPayServer.Views.Wallets;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
@@ -27,6 +28,7 @@ using NBitcoin;
|
|||||||
using NBitcoin.Altcoins;
|
using NBitcoin.Altcoins;
|
||||||
using NBitcoin.Payment;
|
using NBitcoin.Payment;
|
||||||
using NBitpayClient;
|
using NBitpayClient;
|
||||||
|
using NBXplorer.DerivationStrategy;
|
||||||
using NBXplorer.Models;
|
using NBXplorer.Models;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using OpenQA.Selenium;
|
using OpenQA.Selenium;
|
||||||
@@ -41,7 +43,7 @@ namespace BTCPayServer.Tests
|
|||||||
|
|
||||||
public PayJoinTests(ITestOutputHelper helper)
|
public PayJoinTests(ITestOutputHelper helper)
|
||||||
{
|
{
|
||||||
Logs.Tester = new XUnitLog(helper) {Name = "Tests"};
|
Logs.Tester = new XUnitLog(helper) { Name = "Tests" };
|
||||||
Logs.LogProvider = new XUnitLogProvider(helper);
|
Logs.LogProvider = new XUnitLogProvider(helper);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,16 +78,16 @@ namespace BTCPayServer.Tests
|
|||||||
var network = tester.NetworkProvider.GetNetwork<BTCPayNetwork>("BTC");
|
var network = tester.NetworkProvider.GetNetwork<BTCPayNetwork>("BTC");
|
||||||
var repo = tester.PayTester.GetService<PayJoinRepository>();
|
var repo = tester.PayTester.GetService<PayJoinRepository>();
|
||||||
var outpoint = RandomOutpoint();
|
var outpoint = RandomOutpoint();
|
||||||
|
|
||||||
// Should not be locked
|
// Should not be locked
|
||||||
Assert.False(await repo.TryUnlock(outpoint));
|
Assert.False(await repo.TryUnlock(outpoint));
|
||||||
|
|
||||||
// Can lock input
|
// Can lock input
|
||||||
Assert.True(await repo.TryLockInputs(new [] { outpoint }));
|
Assert.True(await repo.TryLockInputs(new[] { outpoint }));
|
||||||
// Can't twice
|
// Can't twice
|
||||||
Assert.False(await repo.TryLockInputs(new [] { outpoint }));
|
Assert.False(await repo.TryLockInputs(new[] { outpoint }));
|
||||||
Assert.False(await repo.TryUnlock(outpoint));
|
Assert.False(await repo.TryUnlock(outpoint));
|
||||||
|
|
||||||
// Lock and unlock outpoint utxo
|
// Lock and unlock outpoint utxo
|
||||||
Assert.True(await repo.TryLock(outpoint));
|
Assert.True(await repo.TryLock(outpoint));
|
||||||
Assert.True(await repo.TryUnlock(outpoint));
|
Assert.True(await repo.TryUnlock(outpoint));
|
||||||
@@ -104,33 +106,33 @@ namespace BTCPayServer.Tests
|
|||||||
var controller = tester.PayTester.GetService<PayJoinEndpointController>();
|
var controller = tester.PayTester.GetService<PayJoinEndpointController>();
|
||||||
|
|
||||||
//Only one utxo, so obvious result
|
//Only one utxo, so obvious result
|
||||||
var utxos = new[] {FakeUTXO(1.0m)};
|
var utxos = new[] { FakeUTXO(1.0m) };
|
||||||
var paymentAmount = 0.5m;
|
var paymentAmount = 0.5m;
|
||||||
var otherOutputs = new[] {0.5m};
|
var otherOutputs = new[] { 0.5m };
|
||||||
var inputs = new[] {1m};
|
var inputs = new[] { 1m };
|
||||||
var result = await controller.SelectUTXO(network, utxos, inputs, paymentAmount, otherOutputs);
|
var result = await controller.SelectUTXO(network, utxos, inputs, paymentAmount, otherOutputs);
|
||||||
Assert.Equal(PayJoinEndpointController.PayjoinUtxoSelectionType.Ordered, result.selectionType);
|
Assert.Equal(PayJoinEndpointController.PayjoinUtxoSelectionType.Ordered, result.selectionType);
|
||||||
Assert.Contains( result.selectedUTXO, utxo => utxos.Contains(utxo));
|
Assert.Contains(result.selectedUTXO, utxo => utxos.Contains(utxo));
|
||||||
|
|
||||||
//no matter what here, no good selection, it seems that payment with 1 utxo generally makes payjoin coin selection unperformant
|
//no matter what here, no good selection, it seems that payment with 1 utxo generally makes payjoin coin selection unperformant
|
||||||
utxos = new[] {FakeUTXO(0.3m),FakeUTXO(0.7m)};
|
utxos = new[] { FakeUTXO(0.3m), FakeUTXO(0.7m) };
|
||||||
paymentAmount = 0.5m;
|
paymentAmount = 0.5m;
|
||||||
otherOutputs = new[] {0.5m};
|
otherOutputs = new[] { 0.5m };
|
||||||
inputs = new[] {1m};
|
inputs = new[] { 1m };
|
||||||
result = await controller.SelectUTXO(network, utxos, inputs, paymentAmount, otherOutputs);
|
result = await controller.SelectUTXO(network, utxos, inputs, paymentAmount, otherOutputs);
|
||||||
Assert.Equal(PayJoinEndpointController.PayjoinUtxoSelectionType.Ordered, result.selectionType);
|
Assert.Equal(PayJoinEndpointController.PayjoinUtxoSelectionType.Ordered, result.selectionType);
|
||||||
|
|
||||||
//when there is no change, anything works
|
//when there is no change, anything works
|
||||||
utxos = new[] {FakeUTXO(1),FakeUTXO(0.1m),FakeUTXO(0.001m),FakeUTXO(0.003m)};
|
utxos = new[] { FakeUTXO(1), FakeUTXO(0.1m), FakeUTXO(0.001m), FakeUTXO(0.003m) };
|
||||||
paymentAmount = 0.5m;
|
paymentAmount = 0.5m;
|
||||||
otherOutputs = new decimal[0];
|
otherOutputs = new decimal[0];
|
||||||
inputs = new[] {0.03m, 0.07m};
|
inputs = new[] { 0.03m, 0.07m };
|
||||||
result = await controller.SelectUTXO(network, utxos, inputs, paymentAmount, otherOutputs);
|
result = await controller.SelectUTXO(network, utxos, inputs, paymentAmount, otherOutputs);
|
||||||
Assert.Equal(PayJoinEndpointController.PayjoinUtxoSelectionType.HeuristicBased, result.selectionType);
|
Assert.Equal(PayJoinEndpointController.PayjoinUtxoSelectionType.HeuristicBased, result.selectionType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private Transaction RandomTransaction(BTCPayNetwork network)
|
private Transaction RandomTransaction(BTCPayNetwork network)
|
||||||
{
|
{
|
||||||
@@ -172,18 +174,18 @@ namespace BTCPayServer.Tests
|
|||||||
var unsupportedFormats = Enum.GetValues(typeof(ScriptPubKeyType))
|
var unsupportedFormats = Enum.GetValues(typeof(ScriptPubKeyType))
|
||||||
.AssertType<ScriptPubKeyType[]>()
|
.AssertType<ScriptPubKeyType[]>()
|
||||||
.Where(type => !PayjoinClient.SupportedFormats.Contains(type));
|
.Where(type => !PayjoinClient.SupportedFormats.Contains(type));
|
||||||
|
|
||||||
|
|
||||||
foreach (ScriptPubKeyType senderAddressType in Enum.GetValues(typeof(ScriptPubKeyType)))
|
foreach (ScriptPubKeyType senderAddressType in Enum.GetValues(typeof(ScriptPubKeyType)))
|
||||||
{
|
{
|
||||||
var senderUser = tester.NewAccount();
|
var senderUser = tester.NewAccount();
|
||||||
senderUser.GrantAccess(true);
|
senderUser.GrantAccess(true);
|
||||||
senderUser.RegisterDerivationScheme("BTC", senderAddressType);
|
senderUser.RegisterDerivationScheme("BTC", senderAddressType);
|
||||||
|
|
||||||
foreach (ScriptPubKeyType receiverAddressType in Enum.GetValues(typeof(ScriptPubKeyType)))
|
foreach (ScriptPubKeyType receiverAddressType in Enum.GetValues(typeof(ScriptPubKeyType)))
|
||||||
{
|
{
|
||||||
var senderCoin = await senderUser.ReceiveUTXO(Money.Satoshis(100000), network);
|
var senderCoin = await senderUser.ReceiveUTXO(Money.Satoshis(100000), network);
|
||||||
|
|
||||||
Logs.Tester.LogInformation($"Testing payjoin with sender: {senderAddressType} receiver: {receiverAddressType}");
|
Logs.Tester.LogInformation($"Testing payjoin with sender: {senderAddressType} receiver: {receiverAddressType}");
|
||||||
var receiverUser = tester.NewAccount();
|
var receiverUser = tester.NewAccount();
|
||||||
receiverUser.GrantAccess(true);
|
receiverUser.GrantAccess(true);
|
||||||
@@ -196,15 +198,16 @@ namespace BTCPayServer.Tests
|
|||||||
if (unsupportedFormats.Contains(receiverAddressType))
|
if (unsupportedFormats.Contains(receiverAddressType))
|
||||||
{
|
{
|
||||||
errorCode = "unsupported-inputs";
|
errorCode = "unsupported-inputs";
|
||||||
}else if (receiverAddressType != senderAddressType)
|
}
|
||||||
|
else if (receiverAddressType != senderAddressType)
|
||||||
{
|
{
|
||||||
errorCode = "out-of-utxos";
|
errorCode = "out-of-utxos";
|
||||||
}
|
}
|
||||||
var invoice = receiverUser.BitPay.CreateInvoice(new Invoice() {Price = 50000, Currency = "sats", FullNotifications = true});
|
var invoice = receiverUser.BitPay.CreateInvoice(new Invoice() { Price = 50000, Currency = "sats", FullNotifications = true });
|
||||||
|
|
||||||
var invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network);
|
var invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network);
|
||||||
var txBuilder = network.NBitcoinNetwork.CreateTransactionBuilder();
|
var txBuilder = network.NBitcoinNetwork.CreateTransactionBuilder();
|
||||||
|
|
||||||
txBuilder.AddCoins(senderCoin);
|
txBuilder.AddCoins(senderCoin);
|
||||||
txBuilder.Send(invoiceAddress, invoice.BtcDue);
|
txBuilder.Send(invoiceAddress, invoice.BtcDue);
|
||||||
txBuilder.SetChange(await senderUser.GetNewAddress(network));
|
txBuilder.SetChange(await senderUser.GetNewAddress(network));
|
||||||
@@ -214,12 +217,12 @@ namespace BTCPayServer.Tests
|
|||||||
var pj = await senderUser.SubmitPayjoin(invoice, psbt, errorCode, clientShouldError);
|
var pj = await senderUser.SubmitPayjoin(invoice, psbt, errorCode, clientShouldError);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
[Trait("Selenium", "Selenium")]
|
[Trait("Selenium", "Selenium")]
|
||||||
public async Task CanUsePayjoinViaUI()
|
public async Task CanUsePayjoinViaUI()
|
||||||
{
|
{
|
||||||
using (var s = SeleniumTester.Create())
|
using (var s = SeleniumTester.Create())
|
||||||
@@ -346,7 +349,7 @@ namespace BTCPayServer.Tests
|
|||||||
.FindElement(By.ClassName("payment-value"));
|
.FindElement(By.ClassName("payment-value"));
|
||||||
Assert.False(paymentValueRowColumn.Text.Contains("payjoin",
|
Assert.False(paymentValueRowColumn.Text.Contains("payjoin",
|
||||||
StringComparison.InvariantCultureIgnoreCase));
|
StringComparison.InvariantCultureIgnoreCase));
|
||||||
|
|
||||||
TestUtils.Eventually(() =>
|
TestUtils.Eventually(() =>
|
||||||
{
|
{
|
||||||
s.GoToWallet(receiverWalletId, WalletsNavPages.Transactions);
|
s.GoToWallet(receiverWalletId, WalletsNavPages.Transactions);
|
||||||
@@ -359,6 +362,126 @@ namespace BTCPayServer.Tests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Integration", "Integration")]
|
||||||
|
public async Task CanUsePayjoin2()
|
||||||
|
{
|
||||||
|
using (var tester = ServerTester.Create())
|
||||||
|
{
|
||||||
|
await tester.StartAsync();
|
||||||
|
var pjClient = tester.PayTester.GetService<PayjoinClient>();
|
||||||
|
var nbx = tester.PayTester.GetService<ExplorerClientProvider>().GetExplorerClient("BTC");
|
||||||
|
var notifications = await nbx.CreateWebsocketNotificationSessionAsync();
|
||||||
|
var alice = tester.NewAccount();
|
||||||
|
await alice.RegisterDerivationSchemeAsync("BTC", ScriptPubKeyType.Segwit, true);
|
||||||
|
await notifications.ListenDerivationSchemesAsync(new[] { alice.DerivationScheme });
|
||||||
|
var address = (await nbx.GetUnusedAsync(alice.DerivationScheme, DerivationFeature.Deposit)).Address;
|
||||||
|
tester.ExplorerNode.SendToAddress(address, Money.Coins(1.0m));
|
||||||
|
await notifications.NextEventAsync();
|
||||||
|
var psbt = (await nbx.CreatePSBTAsync(alice.DerivationScheme, new CreatePSBTRequest()
|
||||||
|
{
|
||||||
|
Destinations =
|
||||||
|
{
|
||||||
|
new CreatePSBTDestination()
|
||||||
|
{
|
||||||
|
Amount = Money.Coins(0.5m),
|
||||||
|
Destination = new Key().PubKey.GetAddress(ScriptPubKeyType.Legacy, Network.RegTest)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
FeePreference = new FeePreference()
|
||||||
|
{
|
||||||
|
ExplicitFee = Money.Satoshis(3000)
|
||||||
|
}
|
||||||
|
})).PSBT;
|
||||||
|
var derivationSchemeSettings = alice.GetController<WalletsController>().GetDerivationSchemeSettings(new WalletId(alice.StoreId, "BTC"));
|
||||||
|
var signingAccount = derivationSchemeSettings.GetSigningAccountKeySettings();
|
||||||
|
psbt.SignAll(derivationSchemeSettings.AccountDerivation, alice.GenerateWalletResponseV.AccountHDKey, signingAccount.GetRootedKeyPath());
|
||||||
|
var changeIndex = Array.FindIndex(psbt.Outputs.ToArray(), (PSBTOutput o) => o.ScriptPubKey.IsScriptType(ScriptType.P2WPKH));
|
||||||
|
using var fakeServer = new FakeServer();
|
||||||
|
await fakeServer.Start();
|
||||||
|
var requesting = pjClient.RequestPayjoin(fakeServer.ServerUri, derivationSchemeSettings, psbt, default);
|
||||||
|
var request = await fakeServer.GetNextRequest();
|
||||||
|
Assert.Equal("1", request.Request.Query["v"][0]);
|
||||||
|
Assert.Equal(changeIndex.ToString(), request.Request.Query["feebumpindex"][0]);
|
||||||
|
Assert.Equal("3000", request.Request.Query["maxfeebumpcontribution"][0]);
|
||||||
|
|
||||||
|
|
||||||
|
Logs.Tester.LogInformation("The payjoin receiver tries to make us pay lots of fee");
|
||||||
|
var originalPSBT = await ParsePSBT(request);
|
||||||
|
var proposalTx = originalPSBT.GetGlobalTransaction();
|
||||||
|
proposalTx.Outputs[changeIndex].Value -= Money.Satoshis(3001);
|
||||||
|
await request.Response.WriteAsync(PSBT.FromTransaction(proposalTx, Network.RegTest).ToBase64(), Encoding.UTF8);
|
||||||
|
fakeServer.Done();
|
||||||
|
var ex = await Assert.ThrowsAsync<PayjoinSenderException>(async () => await requesting);
|
||||||
|
Assert.Contains("too much fee", ex.Message);
|
||||||
|
|
||||||
|
Logs.Tester.LogInformation("The payjoin receiver tries to send money to himself");
|
||||||
|
requesting = pjClient.RequestPayjoin(fakeServer.ServerUri, derivationSchemeSettings, psbt, default);
|
||||||
|
request = await fakeServer.GetNextRequest();
|
||||||
|
originalPSBT = await ParsePSBT(request);
|
||||||
|
proposalTx = originalPSBT.GetGlobalTransaction();
|
||||||
|
proposalTx.Outputs.Where((o, i) => i != changeIndex).First().Value += Money.Satoshis(1);
|
||||||
|
proposalTx.Outputs[changeIndex].Value -= Money.Satoshis(1);
|
||||||
|
await request.Response.WriteAsync(PSBT.FromTransaction(proposalTx, Network.RegTest).ToBase64(), Encoding.UTF8);
|
||||||
|
fakeServer.Done();
|
||||||
|
ex = await Assert.ThrowsAsync<PayjoinSenderException>(async () => await requesting);
|
||||||
|
Assert.Contains("money to himself", ex.Message);
|
||||||
|
|
||||||
|
Logs.Tester.LogInformation("The payjoin receiver can't increase the fee rate too much");
|
||||||
|
requesting = pjClient.RequestPayjoin(fakeServer.ServerUri, derivationSchemeSettings, psbt, default);
|
||||||
|
request = await fakeServer.GetNextRequest();
|
||||||
|
originalPSBT = await ParsePSBT(request);
|
||||||
|
proposalTx = originalPSBT.GetGlobalTransaction();
|
||||||
|
proposalTx.Outputs[changeIndex].Value -= Money.Satoshis(3000);
|
||||||
|
await request.Response.WriteAsync(PSBT.FromTransaction(proposalTx, Network.RegTest).ToBase64(), Encoding.UTF8);
|
||||||
|
fakeServer.Done();
|
||||||
|
ex = await Assert.ThrowsAsync<PayjoinSenderException>(async () => await requesting);
|
||||||
|
Assert.Contains("increased the fee rate", ex.Message);
|
||||||
|
|
||||||
|
Logs.Tester.LogInformation("Make sure the receiver implementation do not take more fee than allowed");
|
||||||
|
var bob = tester.NewAccount();
|
||||||
|
await bob.GrantAccessAsync();
|
||||||
|
await bob.RegisterDerivationSchemeAsync("BTC", ScriptPubKeyType.Segwit, true);
|
||||||
|
await notifications.ListenDerivationSchemesAsync(new[] { bob.DerivationScheme });
|
||||||
|
address = (await nbx.GetUnusedAsync(bob.DerivationScheme, DerivationFeature.Deposit)).Address;
|
||||||
|
tester.ExplorerNode.SendToAddress(address, Money.Coins(1.1m));
|
||||||
|
await notifications.NextEventAsync();
|
||||||
|
bob.ModifyStore(s => s.PayJoinEnabled = true);
|
||||||
|
var invoice = bob.BitPay.CreateInvoice(
|
||||||
|
new Invoice() { Price = 0.1m, Currency = "BTC", FullNotifications = true });
|
||||||
|
var invoiceBIP21 = new BitcoinUrlBuilder(invoice.CryptoInfo.First().PaymentUrls.BIP21,
|
||||||
|
tester.ExplorerClient.Network.NBitcoinNetwork);
|
||||||
|
psbt = (await nbx.CreatePSBTAsync(alice.DerivationScheme, new CreatePSBTRequest()
|
||||||
|
{
|
||||||
|
Destinations =
|
||||||
|
{
|
||||||
|
new CreatePSBTDestination()
|
||||||
|
{
|
||||||
|
Amount = invoiceBIP21.Amount,
|
||||||
|
Destination = invoiceBIP21.Address
|
||||||
|
}
|
||||||
|
},
|
||||||
|
FeePreference = new FeePreference()
|
||||||
|
{
|
||||||
|
ExplicitFee = Money.Satoshis(3001)
|
||||||
|
}
|
||||||
|
})).PSBT;
|
||||||
|
psbt.SignAll(derivationSchemeSettings.AccountDerivation, alice.GenerateWalletResponseV.AccountHDKey, signingAccount.GetRootedKeyPath());
|
||||||
|
var endpoint = TestAccount.GetPayjoinEndpoint(invoice, Network.RegTest);
|
||||||
|
pjClient.MaxFeeBumpContribution = Money.Satoshis(50);
|
||||||
|
var proposal = await pjClient.RequestPayjoin(endpoint, derivationSchemeSettings, psbt, default);
|
||||||
|
Assert.True(proposal.TryGetFee(out var newFee));
|
||||||
|
Assert.Equal(Money.Satoshis(3001 + 50), newFee);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<PSBT> ParsePSBT(Microsoft.AspNetCore.Http.HttpContext request)
|
||||||
|
{
|
||||||
|
var bytes = await request.Request.Body.ReadBytesAsync(int.Parse(request.Request.Headers["Content-Length"].First()));
|
||||||
|
var str = Encoding.UTF8.GetString(bytes);
|
||||||
|
return PSBT.Parse(str, Network.RegTest);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
[Trait("Integration", "Integration")]
|
[Trait("Integration", "Integration")]
|
||||||
public async Task CanUsePayjoinFeeCornerCase()
|
public async Task CanUsePayjoinFeeCornerCase()
|
||||||
@@ -389,7 +512,7 @@ namespace BTCPayServer.Tests
|
|||||||
async Task<PSBT> RunVector(bool skipLockedCheck = false)
|
async Task<PSBT> RunVector(bool skipLockedCheck = false)
|
||||||
{
|
{
|
||||||
var coin = await senderUser.ReceiveUTXO(vector.SpentCoin, network);
|
var coin = await senderUser.ReceiveUTXO(vector.SpentCoin, network);
|
||||||
var invoice = receiverUser.BitPay.CreateInvoice(new Invoice() {Price = vector.InvoiceAmount.ToDecimal(MoneyUnit.BTC), Currency = "BTC", FullNotifications = true});
|
var invoice = receiverUser.BitPay.CreateInvoice(new Invoice() { Price = vector.InvoiceAmount.ToDecimal(MoneyUnit.BTC), Currency = "BTC", FullNotifications = true });
|
||||||
lastInvoiceId = invoice.Id;
|
lastInvoiceId = invoice.Id;
|
||||||
var invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network);
|
var invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network);
|
||||||
var txBuilder = network.NBitcoinNetwork.CreateTransactionBuilder();
|
var txBuilder = network.NBitcoinNetwork.CreateTransactionBuilder();
|
||||||
@@ -447,7 +570,7 @@ namespace BTCPayServer.Tests
|
|||||||
vector = (SpentCoin: Money.Satoshis(810), InvoiceAmount: Money.Satoshis(700), Paid: Money.Satoshis(700), Fee: Money.Satoshis(110), InvoicePaid: true, ExpectedError: "not-enough-money");
|
vector = (SpentCoin: Money.Satoshis(810), InvoiceAmount: Money.Satoshis(700), Paid: Money.Satoshis(700), Fee: Money.Satoshis(110), InvoicePaid: true, ExpectedError: "not-enough-money");
|
||||||
await RunVector();
|
await RunVector();
|
||||||
await LockAllButReceiverCoin();
|
await LockAllButReceiverCoin();
|
||||||
|
|
||||||
Logs.Tester.LogInformation("We don't pay enough");
|
Logs.Tester.LogInformation("We don't pay enough");
|
||||||
vector = (SpentCoin: Money.Satoshis(810), InvoiceAmount: Money.Satoshis(700), Paid: Money.Satoshis(690), Fee: Money.Satoshis(110), InvoicePaid: false, ExpectedError: "invoice-not-fully-paid");
|
vector = (SpentCoin: Money.Satoshis(810), InvoiceAmount: Money.Satoshis(700), Paid: Money.Satoshis(690), Fee: Money.Satoshis(110), InvoicePaid: false, ExpectedError: "invoice-not-fully-paid");
|
||||||
await RunVector();
|
await RunVector();
|
||||||
@@ -456,14 +579,14 @@ namespace BTCPayServer.Tests
|
|||||||
vector = (SpentCoin: Money.Satoshis(810), InvoiceAmount: Money.Satoshis(500), Paid: Money.Satoshis(500), Fee: Money.Satoshis(110), InvoicePaid: true, ExpectedError: null as string);
|
vector = (SpentCoin: Money.Satoshis(810), InvoiceAmount: Money.Satoshis(500), Paid: Money.Satoshis(500), Fee: Money.Satoshis(110), InvoicePaid: true, ExpectedError: null as string);
|
||||||
await RunVector();
|
await RunVector();
|
||||||
await LockAllButReceiverCoin();
|
await LockAllButReceiverCoin();
|
||||||
|
|
||||||
Logs.Tester.LogInformation("We pay a little bit more the invoice with enough fees to support additional input\n" +
|
Logs.Tester.LogInformation("We pay a little bit more the invoice with enough fees to support additional input\n" +
|
||||||
"The receiver should have added a fake output");
|
"The receiver should have added a fake output");
|
||||||
vector = (SpentCoin: Money.Satoshis(910), InvoiceAmount: Money.Satoshis(500), Paid: Money.Satoshis(700), Fee: Money.Satoshis(110), InvoicePaid: true, ExpectedError: null as string);
|
vector = (SpentCoin: Money.Satoshis(910), InvoiceAmount: Money.Satoshis(500), Paid: Money.Satoshis(700), Fee: Money.Satoshis(110), InvoicePaid: true, ExpectedError: null as string);
|
||||||
var proposedPSBT = await RunVector();
|
var proposedPSBT = await RunVector();
|
||||||
Assert.Equal(2, proposedPSBT.Outputs.Count);
|
Assert.Equal(2, proposedPSBT.Outputs.Count);
|
||||||
await LockAllButReceiverCoin();
|
await LockAllButReceiverCoin();
|
||||||
|
|
||||||
Logs.Tester.LogInformation("We pay correctly, but no utxo\n" +
|
Logs.Tester.LogInformation("We pay correctly, but no utxo\n" +
|
||||||
"However, this has the side effect of having the receiver broadcasting the original tx");
|
"However, this has the side effect of having the receiver broadcasting the original tx");
|
||||||
await payjoinRepository.TryLock(receiverCoin.Outpoint);
|
await payjoinRepository.TryLock(receiverCoin.Outpoint);
|
||||||
@@ -511,10 +634,10 @@ namespace BTCPayServer.Tests
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
senderUser = originalSenderUser;
|
senderUser = originalSenderUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Same as above. Except the sender send one satoshi less, so the change
|
// Same as above. Except the sender send one satoshi less, so the change
|
||||||
// output would get below dust and would be removed completely.
|
// output would get below dust and would be removed completely.
|
||||||
// So we remove as much fee as we can, and still accept the transaction because it is above minrelay fee
|
// So we remove as much fee as we can, and still accept the transaction because it is above minrelay fee
|
||||||
@@ -532,7 +655,7 @@ namespace BTCPayServer.Tests
|
|||||||
Assert.True(result.Success);
|
Assert.True(result.Success);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact(Timeout = TestTimeout)]
|
[Fact(Timeout = TestTimeout)]
|
||||||
[Trait("Integration", "Integration")]
|
[Trait("Integration", "Integration")]
|
||||||
public async Task CanUsePayjoin()
|
public async Task CanUsePayjoin()
|
||||||
@@ -540,7 +663,7 @@ namespace BTCPayServer.Tests
|
|||||||
using (var tester = ServerTester.Create())
|
using (var tester = ServerTester.Create())
|
||||||
{
|
{
|
||||||
await tester.StartAsync();
|
await tester.StartAsync();
|
||||||
|
|
||||||
////var payJoinStateProvider = tester.PayTester.GetService<PayJoinStateProvider>();
|
////var payJoinStateProvider = tester.PayTester.GetService<PayJoinStateProvider>();
|
||||||
var btcPayNetwork = tester.NetworkProvider.GetNetwork<BTCPayNetwork>("BTC");
|
var btcPayNetwork = tester.NetworkProvider.GetNetwork<BTCPayNetwork>("BTC");
|
||||||
var btcPayWallet = tester.PayTester.GetService<BTCPayWalletProvider>().GetWallet(btcPayNetwork);
|
var btcPayWallet = tester.PayTester.GetService<BTCPayWalletProvider>().GetWallet(btcPayNetwork);
|
||||||
@@ -552,7 +675,7 @@ namespace BTCPayServer.Tests
|
|||||||
senderUser.RegisterDerivationScheme("BTC", ScriptPubKeyType.Segwit, true);
|
senderUser.RegisterDerivationScheme("BTC", ScriptPubKeyType.Segwit, true);
|
||||||
|
|
||||||
var invoice = senderUser.BitPay.CreateInvoice(
|
var invoice = senderUser.BitPay.CreateInvoice(
|
||||||
new Invoice() {Price = 100, Currency = "USD", FullNotifications = true});
|
new Invoice() { Price = 100, Currency = "USD", FullNotifications = true });
|
||||||
//payjoin is not enabled by default.
|
//payjoin is not enabled by default.
|
||||||
Assert.DoesNotContain($"{PayjoinClient.BIP21EndpointKey}", invoice.CryptoInfo.First().PaymentUrls.BIP21);
|
Assert.DoesNotContain($"{PayjoinClient.BIP21EndpointKey}", invoice.CryptoInfo.First().PaymentUrls.BIP21);
|
||||||
cashCow.SendToAddress(BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network),
|
cashCow.SendToAddress(BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network),
|
||||||
@@ -565,7 +688,7 @@ namespace BTCPayServer.Tests
|
|||||||
await receiverUser.EnablePayJoin();
|
await receiverUser.EnablePayJoin();
|
||||||
// payjoin is enabled, with a segwit wallet, and the keys are available in nbxplorer
|
// payjoin is enabled, with a segwit wallet, and the keys are available in nbxplorer
|
||||||
invoice = receiverUser.BitPay.CreateInvoice(
|
invoice = receiverUser.BitPay.CreateInvoice(
|
||||||
new Invoice() {Price = 0.02m, Currency = "BTC", FullNotifications = true});
|
new Invoice() { Price = 0.02m, Currency = "BTC", FullNotifications = true });
|
||||||
cashCow.SendToAddress(BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network),
|
cashCow.SendToAddress(BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network),
|
||||||
Money.Coins(0.06m));
|
Money.Coins(0.06m));
|
||||||
var receiverWalletId = new WalletId(receiverUser.StoreId, "BTC");
|
var receiverWalletId = new WalletId(receiverUser.StoreId, "BTC");
|
||||||
@@ -587,7 +710,7 @@ namespace BTCPayServer.Tests
|
|||||||
|
|
||||||
//Let's start the harassment
|
//Let's start the harassment
|
||||||
invoice = receiverUser.BitPay.CreateInvoice(
|
invoice = receiverUser.BitPay.CreateInvoice(
|
||||||
new Invoice() {Price = 0.02m, Currency = "BTC", FullNotifications = true});
|
new Invoice() { Price = 0.02m, Currency = "BTC", FullNotifications = true });
|
||||||
// Bad version should throw incorrect version
|
// Bad version should throw incorrect version
|
||||||
var endpoint = TestAccount.GetPayjoinEndpoint(invoice, btcPayNetwork.NBitcoinNetwork);
|
var endpoint = TestAccount.GetPayjoinEndpoint(invoice, btcPayNetwork.NBitcoinNetwork);
|
||||||
var response = await tester.PayTester.HttpClient.PostAsync(endpoint.AbsoluteUri + "?v=2",
|
var response = await tester.PayTester.HttpClient.PostAsync(endpoint.AbsoluteUri + "?v=2",
|
||||||
@@ -601,7 +724,7 @@ namespace BTCPayServer.Tests
|
|||||||
tester.ExplorerClient.Network.NBitcoinNetwork);
|
tester.ExplorerClient.Network.NBitcoinNetwork);
|
||||||
|
|
||||||
var invoice2 = receiverUser.BitPay.CreateInvoice(
|
var invoice2 = receiverUser.BitPay.CreateInvoice(
|
||||||
new Invoice() {Price = 0.02m, Currency = "BTC", FullNotifications = true});
|
new Invoice() { Price = 0.02m, Currency = "BTC", FullNotifications = true });
|
||||||
var secondInvoiceParsedBip21 = new BitcoinUrlBuilder(invoice2.CryptoInfo.First().PaymentUrls.BIP21,
|
var secondInvoiceParsedBip21 = new BitcoinUrlBuilder(invoice2.CryptoInfo.First().PaymentUrls.BIP21,
|
||||||
tester.ExplorerClient.Network.NBitcoinNetwork);
|
tester.ExplorerClient.Network.NBitcoinNetwork);
|
||||||
|
|
||||||
@@ -684,7 +807,7 @@ namespace BTCPayServer.Tests
|
|||||||
//Result: Reject Tx1 but accept tx 2 as its inputs were never accepted by invoice 1
|
//Result: Reject Tx1 but accept tx 2 as its inputs were never accepted by invoice 1
|
||||||
await senderUser.SubmitPayjoin(invoice2, Invoice2Coin1, btcPayNetwork, "inputs-already-used");
|
await senderUser.SubmitPayjoin(invoice2, Invoice2Coin1, btcPayNetwork, "inputs-already-used");
|
||||||
var Invoice2Coin2ResponseTx = await senderUser.SubmitPayjoin(invoice2, Invoice2Coin2, btcPayNetwork);
|
var Invoice2Coin2ResponseTx = await senderUser.SubmitPayjoin(invoice2, Invoice2Coin2, btcPayNetwork);
|
||||||
|
|
||||||
var contributedInputsInvoice2Coin2ResponseTx =
|
var contributedInputsInvoice2Coin2ResponseTx =
|
||||||
Invoice2Coin2ResponseTx.Inputs.Where(txin => coin2.OutPoint != txin.PrevOut);
|
Invoice2Coin2ResponseTx.Inputs.Where(txin => coin2.OutPoint != txin.PrevOut);
|
||||||
Assert.Single(contributedInputsInvoice2Coin2ResponseTx);
|
Assert.Single(contributedInputsInvoice2Coin2ResponseTx);
|
||||||
@@ -693,13 +816,13 @@ namespace BTCPayServer.Tests
|
|||||||
//Result: reject on 4: the protocol should not worry about this complexity
|
//Result: reject on 4: the protocol should not worry about this complexity
|
||||||
|
|
||||||
var invoice3 = receiverUser.BitPay.CreateInvoice(
|
var invoice3 = receiverUser.BitPay.CreateInvoice(
|
||||||
new Invoice() {Price = 0.01m, Currency = "BTC", FullNotifications = true});
|
new Invoice() { Price = 0.01m, Currency = "BTC", FullNotifications = true });
|
||||||
var invoice3ParsedBip21 = new BitcoinUrlBuilder(invoice3.CryptoInfo.First().PaymentUrls.BIP21,
|
var invoice3ParsedBip21 = new BitcoinUrlBuilder(invoice3.CryptoInfo.First().PaymentUrls.BIP21,
|
||||||
tester.ExplorerClient.Network.NBitcoinNetwork);
|
tester.ExplorerClient.Network.NBitcoinNetwork);
|
||||||
|
|
||||||
|
|
||||||
var invoice4 = receiverUser.BitPay.CreateInvoice(
|
var invoice4 = receiverUser.BitPay.CreateInvoice(
|
||||||
new Invoice() {Price = 0.01m, Currency = "BTC", FullNotifications = true});
|
new Invoice() { Price = 0.01m, Currency = "BTC", FullNotifications = true });
|
||||||
var invoice4ParsedBip21 = new BitcoinUrlBuilder(invoice4.CryptoInfo.First().PaymentUrls.BIP21,
|
var invoice4ParsedBip21 = new BitcoinUrlBuilder(invoice4.CryptoInfo.First().PaymentUrls.BIP21,
|
||||||
tester.ExplorerClient.Network.NBitcoinNetwork);
|
tester.ExplorerClient.Network.NBitcoinNetwork);
|
||||||
|
|
||||||
@@ -720,7 +843,7 @@ namespace BTCPayServer.Tests
|
|||||||
//Result: proposed tx consolidates the outputs
|
//Result: proposed tx consolidates the outputs
|
||||||
|
|
||||||
var invoice5 = receiverUser.BitPay.CreateInvoice(
|
var invoice5 = receiverUser.BitPay.CreateInvoice(
|
||||||
new Invoice() {Price = 0.01m, Currency = "BTC", FullNotifications = true});
|
new Invoice() { Price = 0.01m, Currency = "BTC", FullNotifications = true });
|
||||||
var invoice5ParsedBip21 = new BitcoinUrlBuilder(invoice5.CryptoInfo.First().PaymentUrls.BIP21,
|
var invoice5ParsedBip21 = new BitcoinUrlBuilder(invoice5.CryptoInfo.First().PaymentUrls.BIP21,
|
||||||
tester.ExplorerClient.Network.NBitcoinNetwork);
|
tester.ExplorerClient.Network.NBitcoinNetwork);
|
||||||
|
|
||||||
@@ -745,7 +868,7 @@ namespace BTCPayServer.Tests
|
|||||||
new Money(0.1m, MoneyUnit.BTC)));
|
new Money(0.1m, MoneyUnit.BTC)));
|
||||||
|
|
||||||
var invoice6 = receiverUser.BitPay.CreateInvoice(
|
var invoice6 = receiverUser.BitPay.CreateInvoice(
|
||||||
new Invoice() {Price = 0.01m, Currency = "BTC", FullNotifications = true});
|
new Invoice() { Price = 0.01m, Currency = "BTC", FullNotifications = true });
|
||||||
var invoice6ParsedBip21 = new BitcoinUrlBuilder(invoice6.CryptoInfo.First().PaymentUrls.BIP21,
|
var invoice6ParsedBip21 = new BitcoinUrlBuilder(invoice6.CryptoInfo.First().PaymentUrls.BIP21,
|
||||||
tester.ExplorerClient.Network.NBitcoinNetwork);
|
tester.ExplorerClient.Network.NBitcoinNetwork);
|
||||||
|
|
||||||
@@ -760,7 +883,7 @@ namespace BTCPayServer.Tests
|
|||||||
var invoice6Coin5 = invoice6Coin5TxBuilder
|
var invoice6Coin5 = invoice6Coin5TxBuilder
|
||||||
.BuildTransaction(true);
|
.BuildTransaction(true);
|
||||||
|
|
||||||
var Invoice6Coin5Response1Tx =await senderUser.SubmitPayjoin(invoice6, invoice6Coin5, btcPayNetwork);
|
var Invoice6Coin5Response1Tx = await senderUser.SubmitPayjoin(invoice6, invoice6Coin5, btcPayNetwork);
|
||||||
var Invoice6Coin5Response1TxSigned = invoice6Coin5TxBuilder.SignTransaction(Invoice6Coin5Response1Tx);
|
var Invoice6Coin5Response1TxSigned = invoice6Coin5TxBuilder.SignTransaction(Invoice6Coin5Response1Tx);
|
||||||
//broadcast the first payjoin
|
//broadcast the first payjoin
|
||||||
await tester.ExplorerClient.BroadcastAsync(Invoice6Coin5Response1TxSigned);
|
await tester.ExplorerClient.BroadcastAsync(Invoice6Coin5Response1TxSigned);
|
||||||
@@ -788,7 +911,7 @@ namespace BTCPayServer.Tests
|
|||||||
new Money(0.1m, MoneyUnit.BTC)));
|
new Money(0.1m, MoneyUnit.BTC)));
|
||||||
|
|
||||||
var invoice7 = receiverUser.BitPay.CreateInvoice(
|
var invoice7 = receiverUser.BitPay.CreateInvoice(
|
||||||
new Invoice() {Price = 0.01m, Currency = "BTC", FullNotifications = true});
|
new Invoice() { Price = 0.01m, Currency = "BTC", FullNotifications = true });
|
||||||
var invoice7ParsedBip21 = new BitcoinUrlBuilder(invoice7.CryptoInfo.First().PaymentUrls.BIP21,
|
var invoice7ParsedBip21 = new BitcoinUrlBuilder(invoice7.CryptoInfo.First().PaymentUrls.BIP21,
|
||||||
tester.ExplorerClient.Network.NBitcoinNetwork);
|
tester.ExplorerClient.Network.NBitcoinNetwork);
|
||||||
|
|
||||||
@@ -808,14 +931,14 @@ namespace BTCPayServer.Tests
|
|||||||
var Invoice7Coin6Response1TxSigned = invoice7Coin6TxBuilder.SignTransaction(invoice7Coin6Response1Tx);
|
var Invoice7Coin6Response1TxSigned = invoice7Coin6TxBuilder.SignTransaction(invoice7Coin6Response1Tx);
|
||||||
var contributedInputsInvoice7Coin6Response1TxSigned =
|
var contributedInputsInvoice7Coin6Response1TxSigned =
|
||||||
Invoice7Coin6Response1TxSigned.Inputs.Single(txin => coin6.OutPoint != txin.PrevOut);
|
Invoice7Coin6Response1TxSigned.Inputs.Single(txin => coin6.OutPoint != txin.PrevOut);
|
||||||
|
|
||||||
|
|
||||||
////var receiverWalletPayJoinState = payJoinStateProvider.Get(receiverWalletId);
|
////var receiverWalletPayJoinState = payJoinStateProvider.Get(receiverWalletId);
|
||||||
////Assert.Contains(receiverWalletPayJoinState.GetRecords(), item => item.InvoiceId == invoice7.Id);
|
////Assert.Contains(receiverWalletPayJoinState.GetRecords(), item => item.InvoiceId == invoice7.Id);
|
||||||
//broadcast the payjoin
|
//broadcast the payjoin
|
||||||
var res = (await tester.ExplorerClient.BroadcastAsync(Invoice7Coin6Response1TxSigned));
|
var res = (await tester.ExplorerClient.BroadcastAsync(Invoice7Coin6Response1TxSigned));
|
||||||
Assert.True(res.Success);
|
Assert.True(res.Success);
|
||||||
|
|
||||||
// Paid with coinjoin
|
// Paid with coinjoin
|
||||||
await TestUtils.EventuallyAsync(async () =>
|
await TestUtils.EventuallyAsync(async () =>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -145,6 +145,10 @@ namespace BTCPayServer.Tests
|
|||||||
|
|
||||||
public async Task CreateStoreAsync()
|
public async Task CreateStoreAsync()
|
||||||
{
|
{
|
||||||
|
if (UserId is null)
|
||||||
|
{
|
||||||
|
await RegisterAsync();
|
||||||
|
}
|
||||||
var store = this.GetController<UserStoresController>();
|
var store = this.GetController<UserStoresController>();
|
||||||
await store.CreateStore(new CreateStoreViewModel() {Name = "Test Store"});
|
await store.CreateStore(new CreateStoreViewModel() {Name = "Test Store"});
|
||||||
StoreId = store.CreatedStoreId;
|
StoreId = store.CreatedStoreId;
|
||||||
@@ -161,6 +165,8 @@ namespace BTCPayServer.Tests
|
|||||||
public async Task<WalletId> RegisterDerivationSchemeAsync(string cryptoCode, ScriptPubKeyType segwit = ScriptPubKeyType.Legacy,
|
public async Task<WalletId> RegisterDerivationSchemeAsync(string cryptoCode, ScriptPubKeyType segwit = ScriptPubKeyType.Legacy,
|
||||||
bool importKeysToNBX = false)
|
bool importKeysToNBX = false)
|
||||||
{
|
{
|
||||||
|
if (StoreId is null)
|
||||||
|
await CreateStoreAsync();
|
||||||
SupportedNetwork = parent.NetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
|
SupportedNetwork = parent.NetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
|
||||||
var store = parent.PayTester.GetController<StoresController>(UserId, StoreId);
|
var store = parent.PayTester.GetController<StoresController>(UserId, StoreId);
|
||||||
GenerateWalletResponseV = await parent.ExplorerClient.GenerateWalletAsync(new GenerateWalletRequest()
|
GenerateWalletResponseV = await parent.ExplorerClient.GenerateWalletAsync(new GenerateWalletRequest()
|
||||||
|
|||||||
@@ -1023,7 +1023,7 @@ namespace BTCPayServer.Controllers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private DerivationSchemeSettings GetDerivationSchemeSettings(WalletId walletId)
|
internal DerivationSchemeSettings GetDerivationSchemeSettings(WalletId walletId)
|
||||||
{
|
{
|
||||||
var paymentMethod = CurrentStore
|
var paymentMethod = CurrentStore
|
||||||
.GetSupportedPaymentMethods(NetworkProvider)
|
.GetSupportedPaymentMethods(NetworkProvider)
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ namespace BTCPayServer.Payments.PayJoin
|
|||||||
[MediaTypeConstraint("text/plain")]
|
[MediaTypeConstraint("text/plain")]
|
||||||
[RateLimitsFilter(ZoneLimits.PayJoin, Scope = RateLimitsScope.RemoteAddress)]
|
[RateLimitsFilter(ZoneLimits.PayJoin, Scope = RateLimitsScope.RemoteAddress)]
|
||||||
public async Task<IActionResult> Submit(string cryptoCode,
|
public async Task<IActionResult> Submit(string cryptoCode,
|
||||||
bool noadjustfee = false,
|
long maxfeebumpcontribution = -1,
|
||||||
int feebumpindex = -1,
|
int feebumpindex = -1,
|
||||||
int v = 1)
|
int v = 1)
|
||||||
{
|
{
|
||||||
@@ -130,6 +130,7 @@ namespace BTCPayServer.Payments.PayJoin
|
|||||||
new JProperty("message", "This version of payjoin is not supported.")
|
new JProperty("message", "This version of payjoin is not supported.")
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Money allowedFeeBumpContribution = Money.Satoshis(maxfeebumpcontribution >= 0 ? maxfeebumpcontribution : long.MaxValue);
|
||||||
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
|
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
|
||||||
if (network == null)
|
if (network == null)
|
||||||
{
|
{
|
||||||
@@ -415,7 +416,7 @@ namespace BTCPayServer.Payments.PayJoin
|
|||||||
Money expectedFee = txBuilder.EstimateFees(newTx, originalFeeRate);
|
Money expectedFee = txBuilder.EstimateFees(newTx, originalFeeRate);
|
||||||
Money actualFee = newTx.GetFee(txBuilder.FindSpentCoins(newTx));
|
Money actualFee = newTx.GetFee(txBuilder.FindSpentCoins(newTx));
|
||||||
Money additionalFee = expectedFee - actualFee;
|
Money additionalFee = expectedFee - actualFee;
|
||||||
if (additionalFee > Money.Zero && !noadjustfee)
|
if (additionalFee > Money.Zero)
|
||||||
{
|
{
|
||||||
// If the user overpaid, taking fee on our output (useful if sender dump a full UTXO for privacy)
|
// If the user overpaid, taking fee on our output (useful if sender dump a full UTXO for privacy)
|
||||||
for (int i = 0; i < newTx.Outputs.Count && additionalFee > Money.Zero && due < Money.Zero; i++)
|
for (int i = 0; i < newTx.Outputs.Count && additionalFee > Money.Zero && due < Money.Zero; i++)
|
||||||
@@ -433,7 +434,7 @@ namespace BTCPayServer.Payments.PayJoin
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The rest, we take from user's change
|
// The rest, we take from user's change
|
||||||
for (int i = 0; i < newTx.Outputs.Count && additionalFee > Money.Zero; i++)
|
for (int i = 0; i < newTx.Outputs.Count && additionalFee > Money.Zero && allowedFeeBumpContribution > Money.Zero; i++)
|
||||||
{
|
{
|
||||||
if (preferredFeeBumpOutput is TxOut &&
|
if (preferredFeeBumpOutput is TxOut &&
|
||||||
preferredFeeBumpOutput != newTx.Outputs[i])
|
preferredFeeBumpOutput != newTx.Outputs[i])
|
||||||
@@ -443,8 +444,10 @@ namespace BTCPayServer.Payments.PayJoin
|
|||||||
var outputContribution = Money.Min(additionalFee, newTx.Outputs[i].Value);
|
var outputContribution = Money.Min(additionalFee, newTx.Outputs[i].Value);
|
||||||
outputContribution = Money.Min(outputContribution,
|
outputContribution = Money.Min(outputContribution,
|
||||||
newTx.Outputs[i].Value - newTx.Outputs[i].GetDustThreshold(minRelayTxFee));
|
newTx.Outputs[i].Value - newTx.Outputs[i].GetDustThreshold(minRelayTxFee));
|
||||||
|
outputContribution = Money.Min(outputContribution, allowedFeeBumpContribution);
|
||||||
newTx.Outputs[i].Value -= outputContribution;
|
newTx.Outputs[i].Value -= outputContribution;
|
||||||
additionalFee -= outputContribution;
|
additionalFee -= outputContribution;
|
||||||
|
allowedFeeBumpContribution -= outputContribution;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,13 @@ namespace BTCPayServer.Services
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class PayjoinClientParameters
|
||||||
|
{
|
||||||
|
public Money MaxFeeBumpContribution { get; set; }
|
||||||
|
public int? FeeBumpIndex { get; set; }
|
||||||
|
public int Version { get; set; } = 1;
|
||||||
|
}
|
||||||
|
|
||||||
public class PayjoinClient
|
public class PayjoinClient
|
||||||
{
|
{
|
||||||
public const string PayjoinOnionNamedClient = "payjoin.onion";
|
public const string PayjoinOnionNamedClient = "payjoin.onion";
|
||||||
@@ -64,6 +71,8 @@ namespace BTCPayServer.Services
|
|||||||
_httpClientFactory = httpClientFactory;
|
_httpClientFactory = httpClientFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Money MaxFeeBumpContribution { get; set; }
|
||||||
|
|
||||||
public async Task<PSBT> RequestPayjoin(Uri endpoint, DerivationSchemeSettings derivationSchemeSettings,
|
public async Task<PSBT> RequestPayjoin(Uri endpoint, DerivationSchemeSettings derivationSchemeSettings,
|
||||||
PSBT originalTx, CancellationToken cancellationToken)
|
PSBT originalTx, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
@@ -75,13 +84,17 @@ namespace BTCPayServer.Services
|
|||||||
throw new ArgumentNullException(nameof(originalTx));
|
throw new ArgumentNullException(nameof(originalTx));
|
||||||
if (originalTx.IsAllFinalized())
|
if (originalTx.IsAllFinalized())
|
||||||
throw new InvalidOperationException("The original PSBT should not be finalized.");
|
throw new InvalidOperationException("The original PSBT should not be finalized.");
|
||||||
|
var clientParameters = new PayjoinClientParameters();
|
||||||
var type = derivationSchemeSettings.AccountDerivation.ScriptPubKeyType();
|
var type = derivationSchemeSettings.AccountDerivation.ScriptPubKeyType();
|
||||||
if (!SupportedFormats.Contains(type))
|
if (!SupportedFormats.Contains(type))
|
||||||
{
|
{
|
||||||
throw new PayjoinSenderException($"The wallet does not support payjoin");
|
throw new PayjoinSenderException($"The wallet does not support payjoin");
|
||||||
}
|
}
|
||||||
var signingAccount = derivationSchemeSettings.GetSigningAccountKeySettings();
|
var signingAccount = derivationSchemeSettings.GetSigningAccountKeySettings();
|
||||||
|
var changeOutput = originalTx.Outputs.CoinsFor(derivationSchemeSettings.AccountDerivation, signingAccount.AccountKey, signingAccount.GetRootedKeyPath())
|
||||||
|
.FirstOrDefault();
|
||||||
|
if (changeOutput is PSBTOutput o)
|
||||||
|
clientParameters.FeeBumpIndex = (int)o.Index;
|
||||||
var sentBefore = -originalTx.GetBalance(derivationSchemeSettings.AccountDerivation,
|
var sentBefore = -originalTx.GetBalance(derivationSchemeSettings.AccountDerivation,
|
||||||
signingAccount.AccountKey,
|
signingAccount.AccountKey,
|
||||||
signingAccount.GetRootedKeyPath());
|
signingAccount.GetRootedKeyPath());
|
||||||
@@ -89,11 +102,10 @@ namespace BTCPayServer.Services
|
|||||||
if (!originalTx.TryGetEstimatedFeeRate(out var originalFeeRate) || !originalTx.TryGetVirtualSize(out var oldVirtualSize))
|
if (!originalTx.TryGetEstimatedFeeRate(out var originalFeeRate) || !originalTx.TryGetVirtualSize(out var oldVirtualSize))
|
||||||
throw new ArgumentException("originalTx should have utxo information", nameof(originalTx));
|
throw new ArgumentException("originalTx should have utxo information", nameof(originalTx));
|
||||||
var originalFee = originalTx.GetFee();
|
var originalFee = originalTx.GetFee();
|
||||||
|
|
||||||
|
clientParameters.MaxFeeBumpContribution = MaxFeeBumpContribution is null ? originalFee : MaxFeeBumpContribution;
|
||||||
var cloned = originalTx.Clone();
|
var cloned = originalTx.Clone();
|
||||||
if (!cloned.TryFinalize(out var errors))
|
cloned.Finalize();
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We make sure we don't send unnecessary information to the receiver
|
// We make sure we don't send unnecessary information to the receiver
|
||||||
foreach (var finalized in cloned.Inputs.Where(i => i.IsFinalized()))
|
foreach (var finalized in cloned.Inputs.Where(i => i.IsFinalized()))
|
||||||
@@ -107,6 +119,8 @@ namespace BTCPayServer.Services
|
|||||||
}
|
}
|
||||||
|
|
||||||
cloned.GlobalXPubs.Clear();
|
cloned.GlobalXPubs.Clear();
|
||||||
|
|
||||||
|
endpoint = ApplyOptionalParameters(endpoint, clientParameters);
|
||||||
using HttpClient client = CreateHttpClient(endpoint);
|
using HttpClient client = CreateHttpClient(endpoint);
|
||||||
var bpuresponse = await client.PostAsync(endpoint,
|
var bpuresponse = await client.PostAsync(endpoint,
|
||||||
new StringContent(cloned.ToHex(), Encoding.UTF8, "text/plain"), cancellationToken);
|
new StringContent(cloned.ToHex(), Encoding.UTF8, "text/plain"), cancellationToken);
|
||||||
@@ -206,11 +220,6 @@ namespace BTCPayServer.Services
|
|||||||
if (ourInputCount < originalTx.Inputs.Count)
|
if (ourInputCount < originalTx.Inputs.Count)
|
||||||
throw new PayjoinSenderException("The payjoin receiver removed some of our inputs");
|
throw new PayjoinSenderException("The payjoin receiver removed some of our inputs");
|
||||||
|
|
||||||
// We limit the number of inputs the receiver can add
|
|
||||||
var addedInputs = newPSBT.Inputs.Count - originalTx.Inputs.Count;
|
|
||||||
if (addedInputs == 0)
|
|
||||||
throw new PayjoinSenderException("The payjoin receiver did not added any input");
|
|
||||||
|
|
||||||
var sentAfter = -newPSBT.GetBalance(derivationSchemeSettings.AccountDerivation,
|
var sentAfter = -newPSBT.GetBalance(derivationSchemeSettings.AccountDerivation,
|
||||||
signingAccount.AccountKey,
|
signingAccount.AccountKey,
|
||||||
signingAccount.GetRootedKeyPath());
|
signingAccount.GetRootedKeyPath());
|
||||||
@@ -224,8 +233,8 @@ namespace BTCPayServer.Services
|
|||||||
var additionalFee = newPSBT.GetFee() - originalFee;
|
var additionalFee = newPSBT.GetFee() - originalFee;
|
||||||
if (overPaying > additionalFee)
|
if (overPaying > additionalFee)
|
||||||
throw new PayjoinSenderException("The payjoin receiver is sending more money to himself");
|
throw new PayjoinSenderException("The payjoin receiver is sending more money to himself");
|
||||||
if (overPaying > originalFee)
|
if (overPaying > clientParameters.MaxFeeBumpContribution)
|
||||||
throw new PayjoinSenderException("The payjoin receiver is making us pay more than twice the original fee");
|
throw new PayjoinSenderException("The payjoin receiver is making us pay too much fee");
|
||||||
|
|
||||||
// Let's check the difference is only for the fee and that feerate
|
// Let's check the difference is only for the fee and that feerate
|
||||||
// did not changed that much
|
// did not changed that much
|
||||||
@@ -239,6 +248,21 @@ namespace BTCPayServer.Services
|
|||||||
return newPSBT;
|
return newPSBT;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.FeeBumpIndex is int feeBumpIndex)
|
||||||
|
parameters.Add($"feebumpindex={feeBumpIndex}");
|
||||||
|
if (clientParameters.MaxFeeBumpContribution is Money maxFeeBumpContribution)
|
||||||
|
parameters.Add($"maxfeebumpcontribution={maxFeeBumpContribution.Satoshi}");
|
||||||
|
endpoint = new Uri($"{requestUri}?{string.Join('&', parameters)}");
|
||||||
|
return endpoint;
|
||||||
|
}
|
||||||
|
|
||||||
private HttpClient CreateHttpClient(Uri uri)
|
private HttpClient CreateHttpClient(Uri uri)
|
||||||
{
|
{
|
||||||
if (uri.IsOnion())
|
if (uri.IsOnion())
|
||||||
|
|||||||
@@ -144,11 +144,11 @@
|
|||||||
{
|
{
|
||||||
var feeRateOption = Model.RecommendedSatoshiPerByte[index];
|
var feeRateOption = Model.RecommendedSatoshiPerByte[index];
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary crypto-fee-link" value="@feeRateOption.FeeRate" data-toggle="tooltip" title="@feeRateOption.FeeRate sat/b">
|
<button type="button" class="btn btn-sm btn-outline-primary crypto-fee-link" value="@feeRateOption.FeeRate" data-toggle="tooltip" title="@feeRateOption.FeeRate sat/b">
|
||||||
<input type="hidden" asp-for="RecommendedSatoshiPerByte[index].Target" />
|
|
||||||
<input type="hidden" asp-for="RecommendedSatoshiPerByte[index].FeeRate" />
|
|
||||||
@feeRateOption.Target.TimeString()
|
@feeRateOption.Target.TimeString()
|
||||||
|
|
||||||
</button>
|
</button>
|
||||||
|
<input type="hidden" asp-for="RecommendedSatoshiPerByte[index].Target" />
|
||||||
|
<input type="hidden" asp-for="RecommendedSatoshiPerByte[index].FeeRate" />
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user