diff --git a/BTCPayServer.Tests/PayJoinTests.cs b/BTCPayServer.Tests/PayJoinTests.cs index 5f70822a1..f9a2c38b0 100644 --- a/BTCPayServer.Tests/PayJoinTests.cs +++ b/BTCPayServer.Tests/PayJoinTests.cs @@ -28,6 +28,7 @@ using NBitcoin.Altcoins; using NBitcoin.Payment; using NBitpayClient; using NBXplorer.Models; +using Newtonsoft.Json.Linq; using OpenQA.Selenium; using Xunit; using Xunit.Abstractions; @@ -587,6 +588,14 @@ namespace BTCPayServer.Tests //Let's start the harassment invoice = receiverUser.BitPay.CreateInvoice( new Invoice() {Price = 0.02m, Currency = "BTC", FullNotifications = true}); + // Bad version should throw incorrect version + var endpoint = TestAccount.GetPayjoinEndpoint(invoice, btcPayNetwork.NBitcoinNetwork); + var response = await tester.PayTester.HttpClient.PostAsync(endpoint.AbsoluteUri + "?v=2", + new StringContent("", Encoding.UTF8, "text/plain")); + Assert.False(response.IsSuccessStatusCode); + var error = JObject.Parse(await response.Content.ReadAsStringAsync()); + Assert.Equal("version-unsupported", error["errorCode"].Value()); + Assert.Equal(1, ((JArray)error["supported"]).Single().Value()); var parsedBip21 = new BitcoinUrlBuilder(invoice.CryptoInfo.First().PaymentUrls.BIP21, tester.ExplorerClient.Network.NBitcoinNetwork); @@ -665,6 +674,7 @@ namespace BTCPayServer.Tests //Attempt 2: Create two transactions using different inputs and send them to the same invoice. //Result: Second Tx should be rejected. var Invoice1Coin1ResponseTx = await senderUser.SubmitPayjoin(invoice, Invoice1Coin1, btcPayNetwork); + await senderUser.SubmitPayjoin(invoice, Invoice1Coin1, btcPayNetwork, "already-paid"); var contributedInputsInvoice1Coin1ResponseTx = Invoice1Coin1ResponseTx.Inputs.Where(txin => coin.OutPoint != txin.PrevOut); diff --git a/BTCPayServer.Tests/TestAccount.cs b/BTCPayServer.Tests/TestAccount.cs index b8ba0cee9..300426ecb 100644 --- a/BTCPayServer.Tests/TestAccount.cs +++ b/BTCPayServer.Tests/TestAccount.cs @@ -393,7 +393,7 @@ namespace BTCPayServer.Tests return response; } - private static Uri GetPayjoinEndpoint(Invoice invoice, Network network) + public static Uri GetPayjoinEndpoint(Invoice invoice, Network network) { var parsedBip21 = new BitcoinUrlBuilder( invoice.CryptoInfo.First(c => c.CryptoCode == network.NetworkSet.CryptoCode).PaymentUrls.BIP21, diff --git a/BTCPayServer/Payments/PayJoin/PayJoinEndpointController.cs b/BTCPayServer/Payments/PayJoin/PayJoinEndpointController.cs index 2947c56d9..6a7ac61e5 100644 --- a/BTCPayServer/Payments/PayJoin/PayJoinEndpointController.cs +++ b/BTCPayServer/Payments/PayJoin/PayJoinEndpointController.cs @@ -23,6 +23,7 @@ using NBXplorer.DerivationStrategy; using System.Diagnostics.CodeAnalysis; using BTCPayServer.Data; using NBitcoin.DataEncoders; +using Amazon.S3.Model; namespace BTCPayServer.Payments.PayJoin { @@ -115,8 +116,20 @@ namespace BTCPayServer.Payments.PayJoin [EnableCors(CorsPolicies.All)] [MediaTypeConstraint("text/plain")] [RateLimitsFilter(ZoneLimits.PayJoin, Scope = RateLimitsScope.RemoteAddress)] - public async Task Submit(string cryptoCode) + public async Task Submit(string cryptoCode, + bool noadjustfee = false, + int feebumpindex = -1, + int v = 1) { + if (v != 1) + { + return BadRequest(new JObject + { + new JProperty("errorCode", "version-unsupported"), + new JProperty("supported", new JArray(1)), + new JProperty("message", "This version of payjoin is not supported.") + }); + } var network = _btcPayNetworkProvider.GetNetwork(cryptoCode); if (network == null) { @@ -146,26 +159,28 @@ namespace BTCPayServer.Payments.PayJoin Transaction originalTx = null; FeeRate originalFeeRate = null; bool psbtFormat = true; - if (!PSBT.TryParse(rawBody, network.NBitcoinNetwork, out var psbt)) + + if (PSBT.TryParse(rawBody, network.NBitcoinNetwork, out var psbt)) + { + if (!psbt.IsAllFinalized()) + return BadRequest(CreatePayjoinError("psbt-not-finalized", "The PSBT should be finalized")); + originalTx = psbt.ExtractTransaction(); + } + // BTCPay Server implementation support a transaction instead of PSBT + else { psbtFormat = false; if (!Transaction.TryParse(rawBody, network.NBitcoinNetwork, out var tx)) return BadRequest(CreatePayjoinError("invalid-format", "invalid transaction or psbt")); originalTx = tx; psbt = PSBT.FromTransaction(tx, network.NBitcoinNetwork); - psbt = (await explorer.UpdatePSBTAsync(new UpdatePSBTRequest() {PSBT = psbt})).PSBT; + psbt = (await explorer.UpdatePSBTAsync(new UpdatePSBTRequest() { PSBT = psbt })).PSBT; for (int i = 0; i < tx.Inputs.Count; i++) { psbt.Inputs[i].FinalScriptSig = tx.Inputs[i].ScriptSig; psbt.Inputs[i].FinalScriptWitness = tx.Inputs[i].WitScript; } } - else - { - if (!psbt.IsAllFinalized()) - return BadRequest(CreatePayjoinError("psbt-not-finalized", "The PSBT should be finalized")); - originalTx = psbt.ExtractTransaction(); - } async Task BroadcastNow() { @@ -173,8 +188,6 @@ namespace BTCPayServer.Payments.PayJoin } var sendersInputType = psbt.GetInputsScriptPubKeyType(); - if (sendersInputType is null) - return BadRequest(CreatePayjoinError("unsupported-inputs", "Payjoin only support segwit inputs (of the same type)")); if (psbt.CheckSanity() is var errors && errors.Count != 0) { return BadRequest(CreatePayjoinError("insane-psbt", $"This PSBT is insane ({errors[0]})")); @@ -242,7 +255,7 @@ namespace BTCPayServer.Payments.PayJoin //this should never happen, unless the store owner changed the wallet mid way through an invoice return StatusCode(500, CreatePayjoinError("unavailable", $"This service is unavailable for now")); } - if (sendersInputType != receiverInputsType) + if (sendersInputType is ScriptPubKeyType t && t != receiverInputsType) { return StatusCode(503, CreatePayjoinError("out-of-utxos", @@ -331,6 +344,10 @@ namespace BTCPayServer.Payments.PayJoin var ourNewOutput = newTx.Outputs[originalPaymentOutput.Index]; HashSet isOurOutput = new HashSet(); isOurOutput.Add(ourNewOutput); + TxOut preferredFeeBumpOutput = feebumpindex >= 0 + && feebumpindex < newTx.Outputs.Count + && !isOurOutput.Contains(newTx.Outputs[feebumpindex]) + ? newTx.Outputs[feebumpindex] : null; var rand = new Random(); int senderInputCount = newTx.Inputs.Count; foreach (var selectedUTXO in selectedUTXOs.Select(o => o.Value)) @@ -398,7 +415,7 @@ namespace BTCPayServer.Payments.PayJoin Money expectedFee = txBuilder.EstimateFees(newTx, originalFeeRate); Money actualFee = newTx.GetFee(txBuilder.FindSpentCoins(newTx)); Money additionalFee = expectedFee - actualFee; - if (additionalFee > Money.Zero) + if (additionalFee > Money.Zero && !noadjustfee) { // 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++) @@ -418,6 +435,9 @@ namespace BTCPayServer.Payments.PayJoin // The rest, we take from user's change for (int i = 0; i < newTx.Outputs.Count && additionalFee > Money.Zero; i++) { + if (preferredFeeBumpOutput is TxOut && + preferredFeeBumpOutput != newTx.Outputs[i]) + continue; if (!isOurOutput.Contains(newTx.Outputs[i])) { var outputContribution = Money.Min(additionalFee, newTx.Outputs[i].Value); @@ -492,7 +512,8 @@ namespace BTCPayServer.Payments.PayJoin })) .ToDictionary(pair => pair.Key, pair => pair.Value) }); - + + // BTCPay Server support PSBT set as hex if (psbtFormat && HexEncoder.IsWellFormed(rawBody)) { return Ok(newPsbt.ToHex()); @@ -501,6 +522,7 @@ namespace BTCPayServer.Payments.PayJoin { return Ok(newPsbt.ToBase64()); } + // BTCPay Server should returns transaction if received transaction else return Ok(newTx.ToHex()); } diff --git a/BTCPayServer/Services/PayjoinClient.cs b/BTCPayServer/Services/PayjoinClient.cs index 67cc2298c..8acc138a6 100644 --- a/BTCPayServer/Services/PayjoinClient.cs +++ b/BTCPayServer/Services/PayjoinClient.cs @@ -6,9 +6,7 @@ using System.Net.Http; using System.Text; using System.Threading; using System.Threading.Tasks; -using BTCPayServer.Payments.Changelly.Models; using Google.Apis.Http; -using Microsoft.WindowsAzure.Storage.Queue.Protocol; using NBitcoin; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -265,7 +263,8 @@ namespace BTCPayServer.Services OutOfUTXOS, NotEnoughMoney, InsanePSBT, - VersionUnsupported + VersionUnsupported, + NeedUTXOInformation } public class PayjoinReceiverException : PayjoinException { @@ -282,6 +281,7 @@ namespace BTCPayServer.Services "not-enough-money" => PayjoinReceiverWellknownErrors.NotEnoughMoney, "insane-psbt" => PayjoinReceiverWellknownErrors.InsanePSBT, "version-unsupported" => PayjoinReceiverWellknownErrors.VersionUnsupported, + "need-utxo-information" => PayjoinReceiverWellknownErrors.NeedUTXOInformation, _ => null }; } @@ -310,6 +310,7 @@ namespace BTCPayServer.Services "not-enough-money" => "The receiver added some inputs but could not bump the fee of the payjoin proposal.", "insane-psbt" => "Some consistency check on the PSBT failed.", "version-unsupported" => "This version of payjoin is not supported.", + "need-utxo-information" => "The witness UTXO or non witness UTXO is missing", _ => "Unknown error" }; }