Allow payjoin to pay fees from change output
MODIFY
WalletWasabi/Blockchain/Transactions/TransactionFactory.cs
WalletWasabi/Blockchain/Transactions/TransactionFactory.cs
@@ -237,7 +237,7 @@ namespace WalletWasabi.Blockchain.Transactions
// Try to pay using payjoin
if (payjoinClient is { })
{
- psbt = TryNegotiatePayjoin(payjoinClient, builder, psbt);
+ psbt = TryNegotiatePayjoin(payjoinClient, builder, psbt, changeHdPubKey);
}
}
psbt.Finalize();
@@ -310,7 +310,7 @@ namespace WalletWasabi.Blockchain.Transactions
return new BuildTransactionResult(new SmartTransaction(tx, Height.Unknown), psbt, spendsUnconfirmed, sign, fee, feePc, outerWalletOutputs, innerWalletOutputs, spentCoins);
}
- private PSBT TryNegotiatePayjoin(IPayjoinClient payjoinClient, TransactionBuilder builder, PSBT psbt)
+ private PSBT TryNegotiatePayjoin(IPayjoinClient payjoinClient, TransactionBuilder builder, PSBT psbt, HdPubKey changeHdPubKey)
{
try
{
@@ -319,6 +319,7 @@ namespace WalletWasabi.Blockchain.Transactions
psbt = payjoinClient.RequestPayjoin(psbt,
KeyManager.ExtPubKey,
new RootedKeyPath(KeyManager.MasterFingerprint.Value, KeyManager.DefaultAccountKeyPath),
+ changeHdPubKey,
CancellationToken.None).GetAwaiter().GetResult();
builder.SignPSBT(psbt);

MODIFY
WalletWasabi/Helpers/Constants.cs
WalletWasabi/Helpers/Constants.cs
@@ -21,6 +21,7 @@ namespace WalletWasabi.Helpers
public const uint ProtocolVersionWitnessVersion = 70012;
public const int P2wpkhInputSizeInBytes = 41;
+ public const int P2wpkhInputVirtualSizeAccordingBtcPayServer = 68;
public const int P2pkhInputSizeInBytes = 145;
public const int OutputSizeInBytes = 33;

MODIFY
WalletWasabi/WebClients/PayJoin/IPayjoinClient.cs
WalletWasabi/WebClients/PayJoin/IPayjoinClient.cs
@@ -2,6 +2,7 @@ using System;
using System.Threading;
using System.Threading.Tasks;
using NBitcoin;
+using WalletWasabi.Blockchain.Keys;
namespace WalletWasabi.WebClients.PayJoin
{
@@ -9,6 +10,6 @@ namespace WalletWasabi.WebClients.PayJoin
{
Uri PaymentUrl { get; }
- Task<PSBT> RequestPayjoin(PSBT originalTx, IHDKey accountKey, RootedKeyPath rootedKeyPath, CancellationToken cancellationToken);
+ Task<PSBT> RequestPayjoin(PSBT originalTx, IHDKey accountKey, RootedKeyPath rootedKeyPath, HdPubKey changeHdPubKey, CancellationToken cancellationToken);
}
}

MODIFY
WalletWasabi/WebClients/PayJoin/PayjoinClient.cs
WalletWasabi/WebClients/PayJoin/PayjoinClient.cs
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.Linq;
using System.Net;
using System.Net.Http;
@@ -9,6 +10,7 @@ using System.Threading.Tasks;
using NBitcoin;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
+using WalletWasabi.Blockchain.Keys;
using WalletWasabi.Helpers;
using WalletWasabi.TorSocks5;
@@ -34,7 +36,7 @@ namespace WalletWasabi.WebClients.PayJoin
private EndPoint TorSocks5EndPoint { get; }
private ITorHttpClient TorHttpClient { get; }
- public async Task<PSBT> RequestPayjoin(PSBT originalTx, IHDKey accountKey, RootedKeyPath rootedKeyPath, CancellationToken cancellationToken)
+ public async Task<PSBT> RequestPayjoin(PSBT originalTx, IHDKey accountKey, RootedKeyPath rootedKeyPath, HdPubKey changeHdPubKey, CancellationToken cancellationToken)
{
Guard.NotNull(nameof(originalTx), originalTx);
if (originalTx.IsAllFinalized())
@@ -42,15 +44,34 @@ namespace WalletWasabi.WebClients.PayJoin
throw new InvalidOperationException("The original PSBT should not be finalized.");
}
- var sentBefore = -originalTx.GetBalance(ScriptPubKeyType.Segwit, accountKey, rootedKeyPath);
- var oldGlobalTx = originalTx.GetGlobalTransaction();
+ var optionalParameters = new PayjoinClientParameters();
+ var changeOutput = originalTx.Outputs.FirstOrDefault(x => x.ScriptPubKey == changeHdPubKey.P2wpkhScript);
+
+ if (changeOutput is PSBTOutput o)
+ {
+ optionalParameters.AdditionalFeeOutputIndex = (int)o.Index;
+ }
if (!originalTx.TryGetEstimatedFeeRate(out var originalFeeRate) || !originalTx.TryGetVirtualSize(out var oldVirtualSize))
{
throw new ArgumentException("originalTx should have utxo information", nameof(originalTx));
}
-
+/*
+ if (!originalTx.TryGetEstimatedFeeRate(out var originalFeeRate))
+ {
+ throw new ArgumentException("signedPSBT should have utxo information", nameof(originalTx));
+ }
+ */
var originalFee = originalTx.GetFee();
+ optionalParameters.MaxAdditionalFeeContribution =
+ // By default, we want to keep same fee rate and a single additional input
+ originalFeeRate.GetFee(Constants.P2wpkhInputVirtualSizeAccordingBtcPayServer);
+
+ optionalParameters.DisableOutputSubstitution = false;
+
+ var sentBefore = -originalTx.GetBalance(ScriptPubKeyType.Segwit, accountKey, rootedKeyPath);
+ var oldGlobalTx = originalTx.GetGlobalTransaction();
+
var cloned = originalTx.Clone();
if (!cloned.TryFinalize(out var _))
{
@@ -70,7 +91,9 @@ namespace WalletWasabi.WebClients.PayJoin
cloned.GlobalXPubs.Clear();
- var request = new HttpRequestMessage(HttpMethod.Post, PaymentUrl)
+ var endpoint = ApplyOptionalParameters(PaymentUrl, optionalParameters);
+
+ var request = new HttpRequestMessage(HttpMethod.Post, endpoint)
{
Content = new StringContent(cloned.ToHex(), Encoding.UTF8, "text/plain")
};
@@ -251,5 +274,33 @@ namespace WalletWasabi.WebClients.PayJoin
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.AdditionalFeeOutputIndex is int additionalFeeOutputIndex)
+ parameters.Add($"additionalfeeoutputindex={additionalFeeOutputIndex.ToString(CultureInfo.InvariantCulture)}");
+ if (clientParameters.DisableOutputSubstitution is bool disableoutputsubstitution)
+ parameters.Add($"disableoutputsubstitution={disableoutputsubstitution}");
+ if (clientParameters.MaxAdditionalFeeContribution is Money maxAdditionalFeeContribution)
+ parameters.Add($"maxadditionalfeecontribution={maxAdditionalFeeContribution.Satoshi.ToString(CultureInfo.InvariantCulture)}");
+ if (clientParameters.MinFeeRate is FeeRate minFeeRate)
+ parameters.Add($"minfeerate={minFeeRate.SatoshiPerByte.ToString(CultureInfo.InvariantCulture)}");
+ endpoint = new Uri($"{requestUri}?{string.Join('&', parameters)}");
+ return endpoint;
+ }
+ }
+
+ public class PayjoinClientParameters
+ {
+ public Money MaxAdditionalFeeContribution { get; set; }
+ public FeeRate MinFeeRate { get; set; }
+ public int? AdditionalFeeOutputIndex { get; set; }
+ public bool DisableOutputSubstitution { get; set; }
+ public int Version { get; set; } = 1;
}
}

Allow payjoin to pay fees from change output