mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-20 07:24:25 +01:00
Implement noadjustfee, feebumpindex and v parameters for payjoin
This commit is contained in:
@@ -28,6 +28,7 @@ using NBitcoin.Altcoins;
|
|||||||
using NBitcoin.Payment;
|
using NBitcoin.Payment;
|
||||||
using NBitpayClient;
|
using NBitpayClient;
|
||||||
using NBXplorer.Models;
|
using NBXplorer.Models;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
using OpenQA.Selenium;
|
using OpenQA.Selenium;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
using Xunit.Abstractions;
|
using Xunit.Abstractions;
|
||||||
@@ -587,6 +588,14 @@ 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
|
||||||
|
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<string>());
|
||||||
|
Assert.Equal(1, ((JArray)error["supported"]).Single().Value<int>());
|
||||||
|
|
||||||
var parsedBip21 = new BitcoinUrlBuilder(invoice.CryptoInfo.First().PaymentUrls.BIP21,
|
var parsedBip21 = new BitcoinUrlBuilder(invoice.CryptoInfo.First().PaymentUrls.BIP21,
|
||||||
tester.ExplorerClient.Network.NBitcoinNetwork);
|
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.
|
//Attempt 2: Create two transactions using different inputs and send them to the same invoice.
|
||||||
//Result: Second Tx should be rejected.
|
//Result: Second Tx should be rejected.
|
||||||
var Invoice1Coin1ResponseTx = await senderUser.SubmitPayjoin(invoice, Invoice1Coin1, btcPayNetwork);
|
var Invoice1Coin1ResponseTx = await senderUser.SubmitPayjoin(invoice, Invoice1Coin1, btcPayNetwork);
|
||||||
|
|
||||||
await senderUser.SubmitPayjoin(invoice, Invoice1Coin1, btcPayNetwork, "already-paid");
|
await senderUser.SubmitPayjoin(invoice, Invoice1Coin1, btcPayNetwork, "already-paid");
|
||||||
var contributedInputsInvoice1Coin1ResponseTx =
|
var contributedInputsInvoice1Coin1ResponseTx =
|
||||||
Invoice1Coin1ResponseTx.Inputs.Where(txin => coin.OutPoint != txin.PrevOut);
|
Invoice1Coin1ResponseTx.Inputs.Where(txin => coin.OutPoint != txin.PrevOut);
|
||||||
|
|||||||
@@ -393,7 +393,7 @@ namespace BTCPayServer.Tests
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Uri GetPayjoinEndpoint(Invoice invoice, Network network)
|
public static Uri GetPayjoinEndpoint(Invoice invoice, Network network)
|
||||||
{
|
{
|
||||||
var parsedBip21 = new BitcoinUrlBuilder(
|
var parsedBip21 = new BitcoinUrlBuilder(
|
||||||
invoice.CryptoInfo.First(c => c.CryptoCode == network.NetworkSet.CryptoCode).PaymentUrls.BIP21,
|
invoice.CryptoInfo.First(c => c.CryptoCode == network.NetworkSet.CryptoCode).PaymentUrls.BIP21,
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ using NBXplorer.DerivationStrategy;
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
using NBitcoin.DataEncoders;
|
using NBitcoin.DataEncoders;
|
||||||
|
using Amazon.S3.Model;
|
||||||
|
|
||||||
namespace BTCPayServer.Payments.PayJoin
|
namespace BTCPayServer.Payments.PayJoin
|
||||||
{
|
{
|
||||||
@@ -115,8 +116,20 @@ namespace BTCPayServer.Payments.PayJoin
|
|||||||
[EnableCors(CorsPolicies.All)]
|
[EnableCors(CorsPolicies.All)]
|
||||||
[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,
|
||||||
|
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<BTCPayNetwork>(cryptoCode);
|
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
|
||||||
if (network == null)
|
if (network == null)
|
||||||
{
|
{
|
||||||
@@ -146,26 +159,28 @@ namespace BTCPayServer.Payments.PayJoin
|
|||||||
Transaction originalTx = null;
|
Transaction originalTx = null;
|
||||||
FeeRate originalFeeRate = null;
|
FeeRate originalFeeRate = null;
|
||||||
bool psbtFormat = true;
|
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;
|
psbtFormat = false;
|
||||||
if (!Transaction.TryParse(rawBody, network.NBitcoinNetwork, out var tx))
|
if (!Transaction.TryParse(rawBody, network.NBitcoinNetwork, out var tx))
|
||||||
return BadRequest(CreatePayjoinError("invalid-format", "invalid transaction or psbt"));
|
return BadRequest(CreatePayjoinError("invalid-format", "invalid transaction or psbt"));
|
||||||
originalTx = tx;
|
originalTx = tx;
|
||||||
psbt = PSBT.FromTransaction(tx, network.NBitcoinNetwork);
|
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++)
|
for (int i = 0; i < tx.Inputs.Count; i++)
|
||||||
{
|
{
|
||||||
psbt.Inputs[i].FinalScriptSig = tx.Inputs[i].ScriptSig;
|
psbt.Inputs[i].FinalScriptSig = tx.Inputs[i].ScriptSig;
|
||||||
psbt.Inputs[i].FinalScriptWitness = tx.Inputs[i].WitScript;
|
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()
|
async Task BroadcastNow()
|
||||||
{
|
{
|
||||||
@@ -173,8 +188,6 @@ namespace BTCPayServer.Payments.PayJoin
|
|||||||
}
|
}
|
||||||
|
|
||||||
var sendersInputType = psbt.GetInputsScriptPubKeyType();
|
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)
|
if (psbt.CheckSanity() is var errors && errors.Count != 0)
|
||||||
{
|
{
|
||||||
return BadRequest(CreatePayjoinError("insane-psbt", $"This PSBT is insane ({errors[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
|
//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"));
|
return StatusCode(500, CreatePayjoinError("unavailable", $"This service is unavailable for now"));
|
||||||
}
|
}
|
||||||
if (sendersInputType != receiverInputsType)
|
if (sendersInputType is ScriptPubKeyType t && t != receiverInputsType)
|
||||||
{
|
{
|
||||||
return StatusCode(503,
|
return StatusCode(503,
|
||||||
CreatePayjoinError("out-of-utxos",
|
CreatePayjoinError("out-of-utxos",
|
||||||
@@ -331,6 +344,10 @@ namespace BTCPayServer.Payments.PayJoin
|
|||||||
var ourNewOutput = newTx.Outputs[originalPaymentOutput.Index];
|
var ourNewOutput = newTx.Outputs[originalPaymentOutput.Index];
|
||||||
HashSet<TxOut> isOurOutput = new HashSet<TxOut>();
|
HashSet<TxOut> isOurOutput = new HashSet<TxOut>();
|
||||||
isOurOutput.Add(ourNewOutput);
|
isOurOutput.Add(ourNewOutput);
|
||||||
|
TxOut preferredFeeBumpOutput = feebumpindex >= 0
|
||||||
|
&& feebumpindex < newTx.Outputs.Count
|
||||||
|
&& !isOurOutput.Contains(newTx.Outputs[feebumpindex])
|
||||||
|
? newTx.Outputs[feebumpindex] : null;
|
||||||
var rand = new Random();
|
var rand = new Random();
|
||||||
int senderInputCount = newTx.Inputs.Count;
|
int senderInputCount = newTx.Inputs.Count;
|
||||||
foreach (var selectedUTXO in selectedUTXOs.Select(o => o.Value))
|
foreach (var selectedUTXO in selectedUTXOs.Select(o => o.Value))
|
||||||
@@ -398,7 +415,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)
|
if (additionalFee > Money.Zero && !noadjustfee)
|
||||||
{
|
{
|
||||||
// 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++)
|
||||||
@@ -418,6 +435,9 @@ 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; i++)
|
||||||
{
|
{
|
||||||
|
if (preferredFeeBumpOutput is TxOut &&
|
||||||
|
preferredFeeBumpOutput != newTx.Outputs[i])
|
||||||
|
continue;
|
||||||
if (!isOurOutput.Contains(newTx.Outputs[i]))
|
if (!isOurOutput.Contains(newTx.Outputs[i]))
|
||||||
{
|
{
|
||||||
var outputContribution = Money.Min(additionalFee, newTx.Outputs[i].Value);
|
var outputContribution = Money.Min(additionalFee, newTx.Outputs[i].Value);
|
||||||
@@ -493,6 +513,7 @@ namespace BTCPayServer.Payments.PayJoin
|
|||||||
.ToDictionary(pair => pair.Key, pair => pair.Value)
|
.ToDictionary(pair => pair.Key, pair => pair.Value)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// BTCPay Server support PSBT set as hex
|
||||||
if (psbtFormat && HexEncoder.IsWellFormed(rawBody))
|
if (psbtFormat && HexEncoder.IsWellFormed(rawBody))
|
||||||
{
|
{
|
||||||
return Ok(newPsbt.ToHex());
|
return Ok(newPsbt.ToHex());
|
||||||
@@ -501,6 +522,7 @@ namespace BTCPayServer.Payments.PayJoin
|
|||||||
{
|
{
|
||||||
return Ok(newPsbt.ToBase64());
|
return Ok(newPsbt.ToBase64());
|
||||||
}
|
}
|
||||||
|
// BTCPay Server should returns transaction if received transaction
|
||||||
else
|
else
|
||||||
return Ok(newTx.ToHex());
|
return Ok(newTx.ToHex());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,7 @@ using System.Net.Http;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BTCPayServer.Payments.Changelly.Models;
|
|
||||||
using Google.Apis.Http;
|
using Google.Apis.Http;
|
||||||
using Microsoft.WindowsAzure.Storage.Queue.Protocol;
|
|
||||||
using NBitcoin;
|
using NBitcoin;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
@@ -265,7 +263,8 @@ namespace BTCPayServer.Services
|
|||||||
OutOfUTXOS,
|
OutOfUTXOS,
|
||||||
NotEnoughMoney,
|
NotEnoughMoney,
|
||||||
InsanePSBT,
|
InsanePSBT,
|
||||||
VersionUnsupported
|
VersionUnsupported,
|
||||||
|
NeedUTXOInformation
|
||||||
}
|
}
|
||||||
public class PayjoinReceiverException : PayjoinException
|
public class PayjoinReceiverException : PayjoinException
|
||||||
{
|
{
|
||||||
@@ -282,6 +281,7 @@ namespace BTCPayServer.Services
|
|||||||
"not-enough-money" => PayjoinReceiverWellknownErrors.NotEnoughMoney,
|
"not-enough-money" => PayjoinReceiverWellknownErrors.NotEnoughMoney,
|
||||||
"insane-psbt" => PayjoinReceiverWellknownErrors.InsanePSBT,
|
"insane-psbt" => PayjoinReceiverWellknownErrors.InsanePSBT,
|
||||||
"version-unsupported" => PayjoinReceiverWellknownErrors.VersionUnsupported,
|
"version-unsupported" => PayjoinReceiverWellknownErrors.VersionUnsupported,
|
||||||
|
"need-utxo-information" => PayjoinReceiverWellknownErrors.NeedUTXOInformation,
|
||||||
_ => null
|
_ => 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.",
|
"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.",
|
"insane-psbt" => "Some consistency check on the PSBT failed.",
|
||||||
"version-unsupported" => "This version of payjoin is not supported.",
|
"version-unsupported" => "This version of payjoin is not supported.",
|
||||||
|
"need-utxo-information" => "The witness UTXO or non witness UTXO is missing",
|
||||||
_ => "Unknown error"
|
_ => "Unknown error"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user