Fix bug and add already paid invoice check
MODIFY
WalletWasabi.Tests/UnitTests/Transactions/PayjoinTests.cs
WalletWasabi.Tests/UnitTests/Transactions/PayjoinTests.cs
@@ -26,6 +26,19 @@ namespace WalletWasabi.Tests.UnitTests.Transactions
{
public class PayjoinTests
{
+ [Fact]
+ public void ApplyOptionalParametersTest()
+ {
+ var clientParameters = new PayjoinClientParameters();
+ clientParameters.Version = 1;
+ clientParameters.MaxAdditionalFeeContribution = new Money(50, MoneyUnit.MilliBTC);
+
+ Uri result = PayjoinClient.ApplyOptionalParameters(new Uri("http://test.me/btc/?something=1"), clientParameters);
+
+ // Assert that the final URI does not contain `something=1` and that it contains proper parameters (in lowercase!).
+ Assert.Equal("http://test.me/btc/?v=1&disableoutputsubstitution=false&maxadditionalfeecontribution=5000000", result.AbsoluteUri);
+ }
+
[Fact]
public void LazyPayjoinServerTest()
{
@@ -34,7 +47,7 @@ namespace WalletWasabi.Tests.UnitTests.Transactions
// us the fake sense of privacy but it should be valid.
var httpClient = new MockTorHttpClient
{
- OnSendAsync = PayjoinServer(psbt => psbt)
+ OnSendAsync = PayjoinServerOk(psbt => psbt)
};
var payjoinClient = new PayjoinClient(httpClient);
var transactionFactory = CreateTransactionFactory(new[]
@@ -63,7 +76,7 @@ namespace WalletWasabi.Tests.UnitTests.Transactions
// This tests the scenario where the payjoin server behaves as expected.
var httpClient = new MockTorHttpClient
{
- OnSendAsync = PayjoinServer(psbt =>
+ OnSendAsync = PayjoinServerOk(psbt =>
{
var clientTx = psbt.ExtractTransaction();
foreach (var input in clientTx.Inputs)
@@ -133,7 +146,7 @@ namespace WalletWasabi.Tests.UnitTests.Transactions
// This tests the scenario where the payjoin server wants to make us sign one of our own inputs!!!!!.
var httpClient = new MockTorHttpClient
{
- OnSendAsync = PayjoinServer(psbt =>
+ OnSendAsync = PayjoinServerOk(psbt =>
{
var newCoin = psbt.Inputs[0].GetCoin();
newCoin.Outpoint.N = newCoin.Outpoint.N + 1;
@@ -154,7 +167,7 @@ namespace WalletWasabi.Tests.UnitTests.Transactions
// This tests the scenario where the payjoin server wants to make us sign one of our own inputs!!!!!.
httpClient = new MockTorHttpClient
{
- OnSendAsync = PayjoinServer(psbt =>
+ OnSendAsync = PayjoinServerOk(psbt =>
{
var globalTx = psbt.GetGlobalTransaction();
var diff = Money.Coins(0.0007m);
@@ -181,7 +194,7 @@ namespace WalletWasabi.Tests.UnitTests.Transactions
// This tests the scenario where the payjoin server does not clean GloablXPubs.
var httpClient = new MockTorHttpClient
{
- OnSendAsync = PayjoinServer(psbt =>
+ OnSendAsync = PayjoinServerOk(psbt =>
{
var extPubkey = new ExtKey().Neuter().GetWif(Network.Main);
psbt.GlobalXPubs.Add(extPubkey, new RootedKeyPath(extPubkey.GetPublicKey().GetHDFingerPrint(), KeyManager.DefaultAccountKeyPath));
@@ -197,7 +210,7 @@ namespace WalletWasabi.Tests.UnitTests.Transactions
// This tests the scenario where the payjoin server includes keypath info in the inputs.
httpClient = new MockTorHttpClient
{
- OnSendAsync = PayjoinServer(psbt =>
+ OnSendAsync = PayjoinServerOk(psbt =>
{
var extPubkey = new ExtKey().Neuter().GetWif(Network.Main);
psbt.Inputs[0].AddKeyPath(new Key().PubKey, new RootedKeyPath(extPubkey.GetPublicKey().GetHDFingerPrint(), KeyManager.DefaultAccountKeyPath));
@@ -212,7 +225,7 @@ namespace WalletWasabi.Tests.UnitTests.Transactions
// This tests the scenario where the payjoin server modifies the inputs sequence.
httpClient = new MockTorHttpClient
{
- OnSendAsync = PayjoinServer(psbt =>
+ OnSendAsync = PayjoinServerOk(psbt =>
{
var globalTx = psbt.GetGlobalTransaction();
globalTx.Inputs[0].Sequence = globalTx.Inputs[0].Sequence + 1;
@@ -227,7 +240,7 @@ namespace WalletWasabi.Tests.UnitTests.Transactions
// This tests the scenario where the payjoin server returns an unsigned input (fucking bastard).
httpClient = new MockTorHttpClient
{
- OnSendAsync = PayjoinServer(psbt =>
+ OnSendAsync = PayjoinServerOk(psbt =>
{
var globalTx = psbt.GetGlobalTransaction();
globalTx.Inputs.Add(GetRandomOutPoint());
@@ -242,7 +255,7 @@ namespace WalletWasabi.Tests.UnitTests.Transactions
// This tests the scenario where the payjoin server removes one of our inputs (probably to optimize it).
httpClient = new MockTorHttpClient
{
- OnSendAsync = PayjoinServer(psbt =>
+ OnSendAsync = PayjoinServerOk(psbt =>
{
var globalTx = psbt.GetGlobalTransaction();
globalTx.Inputs.Clear(); // remove all the inputs
@@ -258,7 +271,7 @@ namespace WalletWasabi.Tests.UnitTests.Transactions
// This tests the scenario where the payjoin server includes keypath info in the outputs.
httpClient = new MockTorHttpClient
{
- OnSendAsync = PayjoinServer(psbt =>
+ OnSendAsync = PayjoinServerOk(psbt =>
{
var extPubkey = new ExtKey().Neuter().GetWif(Network.Main);
psbt.Outputs[0].AddKeyPath(new Key().PubKey, new RootedKeyPath(extPubkey.GetPublicKey().GetHDFingerPrint(), KeyManager.DefaultAccountKeyPath));
@@ -273,7 +286,7 @@ namespace WalletWasabi.Tests.UnitTests.Transactions
// This tests the scenario where the payjoin server includes partial signatures.
httpClient = new MockTorHttpClient
{
- OnSendAsync = PayjoinServer(psbt =>
+ OnSendAsync = PayjoinServerOk(psbt =>
{
var extPubkey = new ExtKey().Neuter().GetWif(Network.Main);
psbt.Inputs[0].PartialSigs.Add(new Key().PubKey, new TransactionSignature(new Key().Sign(uint256.One)));
@@ -288,7 +301,7 @@ namespace WalletWasabi.Tests.UnitTests.Transactions
// This tests the scenario where the payjoin server modifies the original tx version.
httpClient = new MockTorHttpClient
{
- OnSendAsync = PayjoinServer(psbt =>
+ OnSendAsync = PayjoinServerOk(psbt =>
{
var globalTx = psbt.GetGlobalTransaction();
globalTx.Version += 1;
@@ -303,7 +316,7 @@ namespace WalletWasabi.Tests.UnitTests.Transactions
// This tests the scenario where the payjoin server modifies the original tx locktime value.
httpClient = new MockTorHttpClient
{
- OnSendAsync = PayjoinServer(psbt =>
+ OnSendAsync = PayjoinServerOk(psbt =>
{
var globalTx = psbt.GetGlobalTransaction();
globalTx.LockTime = new LockTime(globalTx.LockTime + 1);
@@ -327,7 +340,7 @@ namespace WalletWasabi.Tests.UnitTests.Transactions
// This tests the scenario where the payjoin server wants to make us sign one of our own inputs!!!!!.
var httpClient = new MockTorHttpClient
{
- OnSendAsync = PayjoinServer(psbt =>
+ OnSendAsync = PayjoinServerOk(psbt =>
{
var globalTx = psbt.GetGlobalTransaction();
var changeOutput = globalTx.Outputs.Single(x => x.ScriptPubKey != destination);
@@ -353,7 +366,7 @@ namespace WalletWasabi.Tests.UnitTests.Transactions
// This tests the scenario where the payjoin server wants to make us sign one of our own inputs!!!!!.
var httpClient = new MockTorHttpClient
{
- OnSendAsync = PayjoinServer(statusCode: HttpStatusCode.InternalServerError)
+ OnSendAsync = PayjoinServerError(statusCode: HttpStatusCode.InternalServerError, "Internal Server Error")
};
var transactionFactory = CreateTransactionFactory(walletCoins);
@@ -361,11 +374,29 @@ namespace WalletWasabi.Tests.UnitTests.Transactions
Assert.Single(tx.Transaction.Transaction.Inputs);
}
- private static Func<HttpMethod, string, NameValueCollection, string, Task<HttpResponseMessage>> PayjoinServer(Func<PSBT, PSBT> transformPsbt = null, HttpStatusCode statusCode = HttpStatusCode.OK)
+ [Fact]
+ public void InvoiceAlreadyPaidTest()
{
- if (transformPsbt is { })
+ // The server wants to make us sign a transaction that pays too much fee.
+ var walletCoins = new[] { ("Pablo", 0, 0.1m, confirmed: true, anonymitySet: 1) };
+ var amountToPay = Money.Coins(0.001m);
+ var destination = new Key().PubKey.WitHash.ScriptPubKey;
+ var payment = new PaymentIntent(destination, amountToPay);
+
+ // This tests the scenario where the payjoin server wants to make us sign one of our own inputs!!!!!.
+ var httpClient = new MockTorHttpClient
{
- return new Func<HttpMethod, string, NameValueCollection, string, Task<HttpResponseMessage>>((method, path, parameters, requestBody) =>
+ OnSendAsync = PayjoinServerError(statusCode: HttpStatusCode.UnprocessableEntity, "already-paid", "The invoice this PSBT is paying has already been partially or completely paid")
+ };
+
+ var transactionFactory = CreateTransactionFactory(walletCoins);
+ var e = Assert.Throws<PayjoinReceiverException>(
+ () => transactionFactory.BuildTransaction(payment, new FeeRate(2m), transactionFactory.Coins.Select(x => x.OutPoint), new PayjoinClient(httpClient)));
+ Assert.Equal("The invoice this PSBT is paying has already been partially or completely paid", e.Message);
+ }
+
+ private static Func<HttpMethod, string, NameValueCollection, string, Task<HttpResponseMessage>> PayjoinServerOk(Func<PSBT, PSBT> transformPsbt = null, HttpStatusCode statusCode = HttpStatusCode.OK)
+ => new Func<HttpMethod, string, NameValueCollection, string, Task<HttpResponseMessage>>((method, path, parameters, requestBody) =>
{
var psbt = PSBT.Parse(requestBody, Network.Main);
var newPsbt = transformPsbt(psbt);
@@ -373,18 +404,15 @@ namespace WalletWasabi.Tests.UnitTests.Transactions
message.Content = new StringContent(newPsbt.ToHex(), Encoding.UTF8, "text/plain");
return Task.FromResult(message);
});
- }
- else
- {
- return new Func<HttpMethod, string, NameValueCollection, string, Task<HttpResponseMessage>>((method, path, parameters, requestBody) =>
+
+ private static Func<HttpMethod, string, NameValueCollection, string, Task<HttpResponseMessage>> PayjoinServerError(HttpStatusCode statusCode, string errorCode, string description="")
+ => new Func<HttpMethod, string, NameValueCollection, string, Task<HttpResponseMessage>>((method, path, parameters, requestBody) =>
{
var message = new HttpResponseMessage(statusCode);
message.ReasonPhrase = "";
- message.Content = new StringContent("");
+ message.Content = new StringContent("{ \"errorCode\": \"" + errorCode + "\", \"message\": \"" + description + "\"}" );
return Task.FromResult(message);
});
- }
- }
private TransactionFactory CreateTransactionFactory(
IEnumerable<(string Label, int KeyIndex, decimal Amount, bool Confirmed, int AnonymitySet)> coins,

MODIFY
WalletWasabi/Blockchain/Transactions/TransactionFactory.cs
WalletWasabi/Blockchain/Transactions/TransactionFactory.cs
@@ -340,6 +340,10 @@ namespace WalletWasabi.Blockchain.Transactions
catch (PayjoinException e)
{
Logger.LogWarning($"Payjoin server responded with {e.Message}. Ignoring...");
+ if (e is PayjoinReceiverException pje && pje.ErrorCode == "already-paid")
+ {
+ throw;
+ }
}
return psbt;

MODIFY
WalletWasabi/Extensions/ExceptionExtensions.cs
WalletWasabi/Extensions/ExceptionExtensions.cs
@@ -21,7 +21,8 @@ namespace System
["invalid password"] = "Wrong password.",
["Invalid wallet name"] = "Invalid wallet name.",
- ["Wallet name is already taken"] = "Wallet name is already taken."
+ ["Wallet name is already taken"] = "Wallet name is already taken.",
+ ["The invoice this PSBT is paying has already been partially or completely paid"] = "The invoice has already been paid"
};
public static string ToTypeMessageString(this Exception ex)

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

MODIFY
WalletWasabi/WebClients/PayJoin/PayjoinClient.cs
WalletWasabi/WebClients/PayJoin/PayjoinClient.cs
@@ -46,13 +46,15 @@ namespace WalletWasabi.WebClients.PayJoin
}
var optionalParameters = new PayjoinClientParameters();
+ if (changeHdPubKey is { })
+ {
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));
@@ -60,7 +62,7 @@ namespace WalletWasabi.WebClients.PayJoin
var originalFee = originalTx.GetFee();
// By default, we want to keep same fee rate and a single additional input
- optionalParameters.MaxAdditionalFeeContribution = originalFeeRate.GetFee(Constants.P2wpkhInputVirtualSizeAccordingBtcPayServer);
+ optionalParameters.MaxAdditionalFeeContribution = originalFeeRate.GetFee(Constants.P2wpkhInputVirtualSize);
optionalParameters.DisableOutputSubstitution = false;
var sentBefore = -originalTx.GetBalance(ScriptPubKeyType.Segwit, accountKey, rootedKeyPath);
@@ -269,7 +271,7 @@ namespace WalletWasabi.WebClients.PayJoin
return newPSBT;
}
- private static Uri ApplyOptionalParameters(Uri endpoint, PayjoinClientParameters clientParameters)
+ internal static Uri ApplyOptionalParameters(Uri endpoint, PayjoinClientParameters clientParameters)
{
var requestUri = endpoint.AbsoluteUri;
if (requestUri.IndexOf('?', StringComparison.OrdinalIgnoreCase) is int i && i != -1)
@@ -284,7 +286,7 @@ namespace WalletWasabi.WebClients.PayJoin
}
if (clientParameters.DisableOutputSubstitution is bool disableoutputsubstitution)
{
- parameters.Add($"disableoutputsubstitution={disableoutputsubstitution}");
+ parameters.Add($"disableoutputsubstitution={(disableoutputsubstitution ? "true" : "false")}");
}
if (clientParameters.MaxAdditionalFeeContribution is Money maxAdditionalFeeContribution)
{

MODIFY
WalletWasabi/WebClients/PayJoin/PayjoinReceiverException.cs
WalletWasabi/WebClients/PayJoin/PayjoinReceiverException.cs
@@ -3,7 +3,7 @@ namespace WalletWasabi.WebClients.PayJoin
public class PayjoinReceiverException : PayjoinException
{
public PayjoinReceiverException(int httpCode, string errorCode, string message)
- : base(FormatMessage(httpCode, errorCode, message))
+ : base(message)
{
HttpCode = httpCode;
ErrorCode = errorCode;

Fix bug and add already paid invoice check