Merge pull request #1581 from NicolasDorier/syncpj

Sync payjoin receiver implementation to the bip
This commit is contained in:
Nicolas Dorier
2020-05-19 21:10:35 +09:00
committed by GitHub
5 changed files with 344 additions and 187 deletions

View File

@@ -194,17 +194,13 @@ namespace BTCPayServer.Tests
var receiverCoin = await receiverUser.ReceiveUTXO(Money.Satoshis(810), network);
var clientShouldError = unsupportedFormats.Contains(senderAddressType);
string errorCode = null;
string errorCode = receiverAddressType == senderAddressType ? null : "unavailable|any UTXO available";
var invoice = receiverUser.BitPay.CreateInvoice(new Invoice() { Price = 50000, Currency = "sats", FullNotifications = true });
if (unsupportedFormats.Contains(receiverAddressType))
{
errorCode = "unsupported-inputs";
Assert.Null(TestAccount.GetPayjoinEndpoint(invoice, cashCow.Network));
continue;
}
else if (receiverAddressType != senderAddressType)
{
errorCode = "out-of-utxos";
}
var invoice = receiverUser.BitPay.CreateInvoice(new Invoice() { Price = 50000, Currency = "sats", FullNotifications = true });
var invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network);
var txBuilder = network.NBitcoinNetwork.CreateTransactionBuilder();
@@ -402,8 +398,8 @@ namespace BTCPayServer.Tests
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]);
Assert.Equal(changeIndex.ToString(), request.Request.Query["additionalfeeoutputindex"][0]);
Assert.Equal("3000", request.Request.Query["maxadditionalfeecontribution"][0]);
Logs.Tester.LogInformation("The payjoin receiver tries to make us pay lots of fee");
@@ -438,6 +434,19 @@ namespace BTCPayServer.Tests
ex = await Assert.ThrowsAsync<PayjoinSenderException>(async () => await requesting);
Assert.Contains("increased the fee rate", ex.Message);
Logs.Tester.LogInformation("The payjoin receiver can't decrease the fee rate too much");
pjClient.MinimumFeeRate = new FeeRate(50m);
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("a too low fee rate", ex.Message);
pjClient.MinimumFeeRate = null;
Logs.Tester.LogInformation("Make sure the receiver implementation do not take more fee than allowed");
var bob = tester.NewAccount();
await bob.GrantAccessAsync();
@@ -472,6 +481,36 @@ namespace BTCPayServer.Tests
var proposal = await pjClient.RequestPayjoin(endpoint, derivationSchemeSettings, psbt, default);
Assert.True(proposal.TryGetFee(out var newFee));
Assert.Equal(Money.Satoshis(3001 + 50), newFee);
proposal = proposal.SignAll(derivationSchemeSettings.AccountDerivation, alice.GenerateWalletResponseV.AccountHDKey, signingAccount.GetRootedKeyPath());
proposal.Finalize();
await tester.ExplorerNode.SendRawTransactionAsync(proposal.ExtractTransaction());
await notifications.NextEventAsync();
Logs.Tester.LogInformation("Abusing minFeeRate should give not enough money error");
invoice = bob.BitPay.CreateInvoice(
new Invoice() { Price = 0.1m, Currency = "BTC", FullNotifications = true });
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());
endpoint = TestAccount.GetPayjoinEndpoint(invoice, Network.RegTest);
pjClient.MinimumFeeRate = new FeeRate(100_000_000.2m);
var ex2 = await Assert.ThrowsAsync<PayjoinReceiverException>(async () => await pjClient.RequestPayjoin(endpoint, derivationSchemeSettings, psbt, default));
Assert.Equal(PayjoinReceiverWellknownErrors.NotEnoughMoney, ex2.WellknownError);
}
}
@@ -508,7 +547,7 @@ namespace BTCPayServer.Tests
var receiverCoin = await receiverUser.ReceiveUTXO(Money.Satoshis(810), network);
string lastInvoiceId = null;
var vector = (SpentCoin: Money.Satoshis(810), InvoiceAmount: Money.Satoshis(700), Paid: Money.Satoshis(700), Fee: Money.Satoshis(110), InvoicePaid: true, ExpectedError: "not-enough-money");
var vector = (SpentCoin: Money.Satoshis(810), InvoiceAmount: Money.Satoshis(700), Paid: Money.Satoshis(700), Fee: Money.Satoshis(110), InvoicePaid: true, ExpectedError: "not-enough-money", OriginalTxBroadcasted: true);
async Task<PSBT> RunVector(bool skipLockedCheck = false)
{
var coin = await senderUser.ReceiveUTXO(vector.SpentCoin, network);
@@ -549,6 +588,19 @@ namespace BTCPayServer.Tests
Assert.Equal("paid", invoice.Status);
});
}
psbt.Finalize();
var broadcasted = await tester.PayTester.GetService<ExplorerClientProvider>().GetExplorerClient("BTC").BroadcastAsync(psbt.ExtractTransaction(), true);
if (vector.OriginalTxBroadcasted)
{
Assert.Equal("txn-already-in-mempool", broadcasted.RPCCodeMessage);
}
else
{
Assert.True(broadcasted.Success);
}
receiverCoin = await receiverUser.ReceiveUTXO(receiverCoin.Amount, network);
await LockAllButReceiverCoin();
return pj;
}
@@ -567,32 +619,35 @@ namespace BTCPayServer.Tests
Logs.Tester.LogInformation("Here we send exactly the right amount. This should fails as\n" +
"there is not enough to pay the additional payjoin input. (going below the min relay fee" +
"However, the original tx has been broadcasted!");
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", OriginalTxBroadcasted: true);
await RunVector();
await LockAllButReceiverCoin();
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", OriginalTxBroadcasted: true);
await RunVector();
Logs.Tester.LogInformation("We pay correctly");
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, OriginalTxBroadcasted: false);
await RunVector();
await LockAllButReceiverCoin();
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");
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();
Assert.Equal(2, proposedPSBT.Outputs.Count);
await LockAllButReceiverCoin();
PSBT proposedPSBT = null;
var outputCountReceived = new bool[2];
do
{
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");
vector = (SpentCoin: Money.Satoshis(910), InvoiceAmount: Money.Satoshis(500), Paid: Money.Satoshis(700), Fee: Money.Satoshis(110), InvoicePaid: true, ExpectedError: null as string, OriginalTxBroadcasted: false);
proposedPSBT = await RunVector();
Assert.True(proposedPSBT.Outputs.Count == 1 || proposedPSBT.Outputs.Count == 2);
outputCountReceived[proposedPSBT.Outputs.Count - 1] = true;
cashCow.Generate(1);
} while (outputCountReceived.All(o => o));
Logs.Tester.LogInformation("We pay correctly, but no utxo\n" +
"However, this has the side effect of having the receiver broadcasting the original tx");
await payjoinRepository.TryLock(receiverCoin.Outpoint);
vector = (SpentCoin: Money.Satoshis(810), InvoiceAmount: Money.Satoshis(500), Paid: Money.Satoshis(500), Fee: Money.Satoshis(110), InvoicePaid: true, ExpectedError: "out-of-utxos");
vector = (SpentCoin: Money.Satoshis(810), InvoiceAmount: Money.Satoshis(500), Paid: Money.Satoshis(500), Fee: Money.Satoshis(110), InvoicePaid: true, ExpectedError: "unavailable|any UTXO available", OriginalTxBroadcasted: true);
await RunVector(true);
await LockAllButReceiverCoin();
var originalSenderUser = senderUser;
retry:
@@ -602,7 +657,7 @@ namespace BTCPayServer.Tests
// The send pay remaining 86 sat from his pocket
// So total paid by sender should be 86 + 510 + 200 so we should get 1090 - (86 + 510 + 200) == 294 back)
Logs.Tester.LogInformation($"Check if we can take fee on overpaid utxo{(senderUser == receiverUser ? " (to self)" : "")}");
vector = (SpentCoin: Money.Satoshis(1090), InvoiceAmount: Money.Satoshis(500), Paid: Money.Satoshis(510), Fee: Money.Satoshis(200), InvoicePaid: true, ExpectedError: null as string);
vector = (SpentCoin: Money.Satoshis(1090), InvoiceAmount: Money.Satoshis(500), Paid: Money.Satoshis(510), Fee: Money.Satoshis(200), InvoicePaid: true, ExpectedError: null as string, OriginalTxBroadcasted: false);
proposedPSBT = await RunVector();
Assert.Equal(2, proposedPSBT.Outputs.Count);
Assert.Contains(proposedPSBT.Outputs, o => o.Value == Money.Satoshis(500) + receiverCoin.Amount);
@@ -641,7 +696,7 @@ namespace BTCPayServer.Tests
// Same as above. Except the sender send one satoshi less, so the change
// 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
vector = (SpentCoin: Money.Satoshis(1089), InvoiceAmount: Money.Satoshis(500), Paid: Money.Satoshis(510), Fee: Money.Satoshis(200), InvoicePaid: true, ExpectedError: null as string);
vector = (SpentCoin: Money.Satoshis(1089), InvoiceAmount: Money.Satoshis(500), Paid: Money.Satoshis(510), Fee: Money.Satoshis(200), InvoicePaid: true, ExpectedError: null as string, OriginalTxBroadcasted: false);
proposedPSBT = await RunVector();
Assert.Equal(2, proposedPSBT.Outputs.Count);
// We should have our payment
@@ -805,7 +860,7 @@ namespace BTCPayServer.Tests
//Attempt 3: Send the same inputs from invoice 1 to invoice 2 while invoice 1 tx has not been broadcasted
//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, expectedError: "unavailable|Some of those inputs have already been used");
var Invoice2Coin2ResponseTx = await senderUser.SubmitPayjoin(invoice2, Invoice2Coin2, btcPayNetwork);
var contributedInputsInvoice2Coin2ResponseTx =

View File

@@ -136,6 +136,13 @@ namespace BTCPayServer.Tests
modify(store);
storeController.UpdateStore(store).GetAwaiter().GetResult();
}
public Task ModifyStoreAsync(Action<StoreViewModel> modify)
{
var storeController = GetController<StoresController>();
StoreViewModel store = (StoreViewModel)((ViewResult)storeController.UpdateStore()).Model;
modify(store);
return storeController.UpdateStore(store);
}
public T GetController<T>(bool setImplicitStore = true) where T : Controller
{
@@ -192,18 +199,9 @@ namespace BTCPayServer.Tests
return new WalletId(StoreId, cryptoCode);
}
public async Task EnablePayJoin()
public Task EnablePayJoin()
{
var storeController = parent.PayTester.GetController<StoresController>(UserId, StoreId);
var storeVM =
Assert.IsType<StoreViewModel>(Assert
.IsType<ViewResult>(storeController.UpdateStore()).Model);
storeVM.PayJoinEnabled = true;
Assert.Equal(nameof(storeController.UpdateStore),
Assert.IsType<RedirectToActionResult>(
await storeController.UpdateStore(storeVM)).ActionName);
return ModifyStoreAsync(s => s.PayJoinEnabled = true);
}
public GenerateWalletResponse GenerateWalletResponseV { get; set; }
@@ -332,7 +330,7 @@ namespace BTCPayServer.Tests
var endpoint = GetPayjoinEndpoint(invoice, psbt.Network);
if (endpoint == null)
{
return null;
throw new InvalidOperationException("No payjoin endpoint for the invoice");
}
var pjClient = parent.PayTester.GetService<PayjoinClient>();
var storeRepository = parent.PayTester.GetService<StoreRepository>();
@@ -356,7 +354,10 @@ namespace BTCPayServer.Tests
else
{
var ex = await Assert.ThrowsAsync<PayjoinReceiverException>(async () => await pjClient.RequestPayjoin(endpoint, settings, psbt, default));
Assert.Equal(expectedError, ex.ErrorCode);
var split = expectedError.Split('|');
Assert.Equal(split[0], ex.ErrorCode);
if (split.Length > 1)
Assert.Contains(split[1], ex.ReceiverMessage);
}
return null;
}
@@ -381,9 +382,13 @@ namespace BTCPayServer.Tests
new StringContent(content, Encoding.UTF8, "text/plain"));
if (expectedError != null)
{
var split = expectedError.Split('|');
Assert.False(response.IsSuccessStatusCode);
var error = JObject.Parse(await response.Content.ReadAsStringAsync());
Assert.Equal(expectedError, error["errorCode"].Value<string>());
if (split.Length > 0)
Assert.Equal(split[0], error["errorCode"].Value<string>());
if (split.Length > 1)
Assert.Contains(split[1], error["message"].Value<string>());
return null;
}
else

View File

@@ -24,6 +24,7 @@ using System.Diagnostics.CodeAnalysis;
using BTCPayServer.Data;
using NBitcoin.DataEncoders;
using Amazon.S3.Model;
using BTCPayServer.Logging;
namespace BTCPayServer.Payments.PayJoin
{
@@ -89,6 +90,7 @@ namespace BTCPayServer.Payments.PayJoin
private readonly NBXplorerDashboard _dashboard;
private readonly DelayedTransactionBroadcaster _broadcaster;
private readonly WalletRepository _walletRepository;
private readonly BTCPayServerEnvironment _env;
public PayJoinEndpointController(BTCPayNetworkProvider btcPayNetworkProvider,
InvoiceRepository invoiceRepository, ExplorerClientProvider explorerClientProvider,
@@ -97,7 +99,8 @@ namespace BTCPayServer.Payments.PayJoin
EventAggregator eventAggregator,
NBXplorerDashboard dashboard,
DelayedTransactionBroadcaster broadcaster,
WalletRepository walletRepository)
WalletRepository walletRepository,
BTCPayServerEnvironment env)
{
_btcPayNetworkProvider = btcPayNetworkProvider;
_invoiceRepository = invoiceRepository;
@@ -109,6 +112,7 @@ namespace BTCPayServer.Payments.PayJoin
_dashboard = dashboard;
_broadcaster = broadcaster;
_walletRepository = walletRepository;
_env = env;
}
[HttpPost("")]
@@ -117,8 +121,9 @@ namespace BTCPayServer.Payments.PayJoin
[MediaTypeConstraint("text/plain")]
[RateLimitsFilter(ZoneLimits.PayJoin, Scope = RateLimitsScope.RemoteAddress)]
public async Task<IActionResult> Submit(string cryptoCode,
long maxfeebumpcontribution = -1,
int feebumpindex = -1,
long maxadditionalfeecontribution = -1,
int additionalfeeoutputindex = -1,
decimal minfeerate = -1.0m,
int v = 1)
{
if (v != 1)
@@ -130,13 +135,18 @@ namespace BTCPayServer.Payments.PayJoin
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);
if (network == null)
{
return BadRequest(CreatePayjoinError("invalid-network", "Incorrect network"));
}
await using var ctx = new PayjoinReceiverContext(_invoiceRepository, _explorerClientProvider.GetExplorerClient(network), _payJoinRepository);
ObjectResult CreatePayjoinErrorAndLog(int httpCode, PayjoinReceiverWellknownErrors err, string debug)
{
ctx.Logs.Write($"Payjoin error: {debug}");
return StatusCode(httpCode, CreatePayjoinError(err, debug));
}
var explorer = _explorerClientProvider.GetExplorerClient(network);
if (Request.ContentLength is long length)
{
@@ -157,7 +167,6 @@ namespace BTCPayServer.Payments.PayJoin
rawBody = (await reader.ReadToEndAsync()) ?? string.Empty;
}
Transaction originalTx = null;
FeeRate originalFeeRate = null;
bool psbtFormat = true;
@@ -165,7 +174,7 @@ namespace BTCPayServer.Payments.PayJoin
{
if (!psbt.IsAllFinalized())
return BadRequest(CreatePayjoinError("psbt-not-finalized", "The PSBT should be finalized"));
originalTx = psbt.ExtractTransaction();
ctx.OriginalTransaction = psbt.ExtractTransaction();
}
// BTCPay Server implementation support a transaction instead of PSBT
else
@@ -173,7 +182,7 @@ namespace BTCPayServer.Payments.PayJoin
psbtFormat = false;
if (!Transaction.TryParse(rawBody, network.NBitcoinNetwork, out var tx))
return BadRequest(CreatePayjoinError("invalid-format", "invalid transaction or psbt"));
originalTx = tx;
ctx.OriginalTransaction = tx;
psbt = PSBT.FromTransaction(tx, network.NBitcoinNetwork);
psbt = (await explorer.UpdatePSBTAsync(new UpdatePSBTRequest() { PSBT = psbt })).PSBT;
for (int i = 0; i < tx.Inputs.Count; i++)
@@ -183,10 +192,9 @@ namespace BTCPayServer.Payments.PayJoin
}
}
async Task BroadcastNow()
{
await _explorerClientProvider.GetExplorerClient(network).BroadcastAsync(originalTx);
}
bool spareChangeCase = psbt.Outputs.Count == 1;
FeeRate senderMinFeeRate = !spareChangeCase && minfeerate >= 0.0m ? new FeeRate(minfeerate) : null;
Money allowedSenderFeeContribution = Money.Satoshis(!spareChangeCase && maxadditionalfeecontribution >= 0 ? maxadditionalfeecontribution : long.MaxValue);
var sendersInputType = psbt.GetInputsScriptPubKeyType();
if (psbt.CheckSanity() is var errors && errors.Count != 0)
@@ -219,9 +227,10 @@ namespace BTCPayServer.Payments.PayJoin
}
////////////
var mempool = await explorer.BroadcastAsync(originalTx, true);
var mempool = await explorer.BroadcastAsync(ctx.OriginalTransaction, true);
if (!mempool.Success)
{
ctx.DoNotBroadcast();
return BadRequest(CreatePayjoinError("invalid-transaction",
$"Provided transaction isn't mempool eligible {mempool.RPCCodeMessage}"));
}
@@ -230,11 +239,6 @@ namespace BTCPayServer.Payments.PayJoin
bool paidSomething = false;
Money due = null;
Dictionary<OutPoint, UTXO> selectedUTXOs = new Dictionary<OutPoint, UTXO>();
async Task UnlockUTXOs()
{
await _payJoinRepository.TryUnlock(selectedUTXOs.Select(o => o.Key).ToArray());
}
PSBTOutput originalPaymentOutput = null;
BitcoinAddress paymentAddress = null;
InvoiceEntity invoice = null;
@@ -242,7 +246,7 @@ namespace BTCPayServer.Payments.PayJoin
foreach (var output in psbt.Outputs)
{
var key = output.ScriptPubKey.Hash + "#" + network.CryptoCode.ToUpperInvariant();
invoice = (await _invoiceRepository.GetInvoicesFromAddresses(new[] {key})).FirstOrDefault();
invoice = (await _invoiceRepository.GetInvoicesFromAddresses(new[] { key })).FirstOrDefault();
if (invoice is null)
continue;
derivationSchemeSettings = invoice.GetSupportedPaymentMethod<DerivationSchemeSettings>(paymentMethodId)
@@ -254,13 +258,11 @@ namespace BTCPayServer.Payments.PayJoin
if (!PayjoinClient.SupportedFormats.Contains(receiverInputsType))
{
//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 CreatePayjoinErrorAndLog(503, PayjoinReceiverWellknownErrors.Unavailable, "Our wallet does not support payjoin");
}
if (sendersInputType is ScriptPubKeyType t && t != receiverInputsType)
{
return StatusCode(503,
CreatePayjoinError("out-of-utxos",
"We do not have any UTXO available for making a payjoin with the sender's inputs type"));
return CreatePayjoinErrorAndLog(503, PayjoinReceiverWellknownErrors.Unavailable, "We do not have any UTXO available for making a payjoin with the sender's inputs type");
}
var paymentMethod = invoice.GetPaymentMethod(paymentMethodId);
var paymentDetails =
@@ -269,6 +271,7 @@ namespace BTCPayServer.Payments.PayJoin
continue;
if (invoice.GetAllBitcoinPaymentData().Any())
{
ctx.DoNotBroadcast();
return UnprocessableEntity(CreatePayjoinError("already-paid",
$"The invoice this PSBT is paying has already been partially or completely paid"));
}
@@ -280,26 +283,24 @@ namespace BTCPayServer.Payments.PayJoin
break;
}
if (!await _payJoinRepository.TryLockInputs(originalTx.Inputs.Select(i => i.PrevOut).ToArray()))
if (!await _payJoinRepository.TryLockInputs(ctx.OriginalTransaction.Inputs.Select(i => i.PrevOut).ToArray()))
{
return BadRequest(CreatePayjoinError("inputs-already-used",
"Some of those inputs have already been used to make payjoin transaction"));
return CreatePayjoinErrorAndLog(503, PayjoinReceiverWellknownErrors.Unavailable, "Some of those inputs have already been used to make another payjoin transaction");
}
var utxos = (await explorer.GetUTXOsAsync(derivationSchemeSettings.AccountDerivation))
.GetUnspentUTXOs(false);
// In case we are paying ourselves, be need to make sure
// we can't take spent outpoints.
var prevOuts = originalTx.Inputs.Select(o => o.PrevOut).ToHashSet();
var prevOuts = ctx.OriginalTransaction.Inputs.Select(o => o.PrevOut).ToHashSet();
utxos = utxos.Where(u => !prevOuts.Contains(u.Outpoint)).ToArray();
Array.Sort(utxos, UTXODeterministicComparer.Instance);
foreach (var utxo in (await SelectUTXO(network, utxos, psbt.Inputs.Select(input => input.WitnessUtxo.Value.ToDecimal(MoneyUnit.BTC)), output.Value.ToDecimal(MoneyUnit.BTC),
foreach (var utxo in (await SelectUTXO(network, utxos, psbt.Inputs.Select(input => input.WitnessUtxo.Value.ToDecimal(MoneyUnit.BTC)), output.Value.ToDecimal(MoneyUnit.BTC),
psbt.Outputs.Where(psbtOutput => psbtOutput.Index != output.Index).Select(psbtOutput => psbtOutput.Value.ToDecimal(MoneyUnit.BTC)))).selectedUTXO)
{
selectedUTXOs.Add(utxo.Outpoint, utxo);
}
ctx.LockedUTXOs = selectedUTXOs.Select(u => u.Key).ToArray();
originalPaymentOutput = output;
paymentAddress = paymentDetails.GetDepositAddress(network.NBitcoinNetwork);
break;
@@ -319,14 +320,11 @@ namespace BTCPayServer.Payments.PayJoin
if (selectedUTXOs.Count == 0)
{
await BroadcastNow();
return StatusCode(503,
CreatePayjoinError("out-of-utxos",
"We do not have any UTXO available for making a payjoin for now"));
return CreatePayjoinErrorAndLog(503, PayjoinReceiverWellknownErrors.Unavailable, "We do not have any UTXO available for contributing to a payjoin");
}
var originalPaymentValue = originalPaymentOutput.Value;
await _broadcaster.Schedule(DateTimeOffset.UtcNow + TimeSpan.FromMinutes(2.0), originalTx, network);
await _broadcaster.Schedule(DateTimeOffset.UtcNow + TimeSpan.FromMinutes(2.0), ctx.OriginalTransaction, network);
//check if wallet of store is configured to be hot wallet
var extKeyStr = await explorer.GetMetadataAsync<string>(
@@ -335,20 +333,18 @@ namespace BTCPayServer.Payments.PayJoin
if (extKeyStr == null)
{
// This should not happen, as we check the existance of private key before creating invoice with payjoin
await UnlockUTXOs();
await BroadcastNow();
return StatusCode(500, CreatePayjoinError("unavailable", $"This service is unavailable for now"));
return CreatePayjoinErrorAndLog(503, PayjoinReceiverWellknownErrors.Unavailable, "The HD Key of the store changed");
}
Money contributedAmount = Money.Zero;
var newTx = originalTx.Clone();
var newTx = ctx.OriginalTransaction.Clone();
var ourNewOutput = newTx.Outputs[originalPaymentOutput.Index];
HashSet<TxOut> isOurOutput = new HashSet<TxOut>();
isOurOutput.Add(ourNewOutput);
TxOut preferredFeeBumpOutput = feebumpindex >= 0
&& feebumpindex < newTx.Outputs.Count
&& !isOurOutput.Contains(newTx.Outputs[feebumpindex])
? newTx.Outputs[feebumpindex] : null;
TxOut preferredFeeBumpOutput = additionalfeeoutputindex >= 0
&& additionalfeeoutputindex < newTx.Outputs.Count
&& !isOurOutput.Contains(newTx.Outputs[additionalfeeoutputindex])
? newTx.Outputs[additionalfeeoutputindex] : null;
var rand = new Random();
int senderInputCount = newTx.Inputs.Count;
foreach (var selectedUTXO in selectedUTXOs.Select(o => o.Value))
@@ -360,40 +356,44 @@ namespace BTCPayServer.Payments.PayJoin
ourNewOutput.Value += contributedAmount;
var minRelayTxFee = this._dashboard.Get(network.CryptoCode).Status.BitcoinStatus?.MinRelayTxFee ??
new FeeRate(1.0m);
// Probably receiving some spare change, let's add an output to make
// it looks more like a normal transaction
if (newTx.Outputs.Count == 1)
if (spareChangeCase)
{
var change = await explorer.GetUnusedAsync(derivationSchemeSettings.AccountDerivation, DerivationFeature.Change);
var randomChangeAmount = RandomUtils.GetUInt64() % (ulong)contributedAmount.Satoshi;
ctx.Logs.Write($"The payjoin receiver sent only a single output");
if (RandomUtils.GetUInt64() % 2 == 0)
{
var change = await explorer.GetUnusedAsync(derivationSchemeSettings.AccountDerivation, DerivationFeature.Change);
var randomChangeAmount = RandomUtils.GetUInt64() % (ulong)contributedAmount.Satoshi;
// Randomly round the amount to make the payment output look like a change output
var roundMultiple = (ulong)Math.Pow(10, (ulong)Math.Log10(randomChangeAmount));
while (roundMultiple > 1_000UL)
{
if (RandomUtils.GetUInt32() % 2 == 0)
// Randomly round the amount to make the payment output look like a change output
var roundMultiple = (ulong)Math.Pow(10, (ulong)Math.Log10(randomChangeAmount));
while (roundMultiple > 1_000UL)
{
roundMultiple = roundMultiple / 10;
if (RandomUtils.GetUInt32() % 2 == 0)
{
roundMultiple = roundMultiple / 10;
}
else
{
randomChangeAmount = (randomChangeAmount / roundMultiple) * roundMultiple;
break;
}
}
else
{
randomChangeAmount = (randomChangeAmount / roundMultiple) * roundMultiple;
break;
}
}
var fakeChange = newTx.Outputs.CreateNewTxOut(randomChangeAmount, change.ScriptPubKey);
if (fakeChange.IsDust(minRelayTxFee))
{
randomChangeAmount = fakeChange.GetDustThreshold(minRelayTxFee);
fakeChange.Value = randomChangeAmount;
}
if (randomChangeAmount < contributedAmount)
{
ourNewOutput.Value -= fakeChange.Value;
newTx.Outputs.Add(fakeChange);
isOurOutput.Add(fakeChange);
var fakeChange = newTx.Outputs.CreateNewTxOut(randomChangeAmount, change.ScriptPubKey);
if (fakeChange.IsDust(minRelayTxFee))
{
randomChangeAmount = fakeChange.GetDustThreshold(minRelayTxFee);
fakeChange.Value = randomChangeAmount;
}
if (randomChangeAmount < contributedAmount)
{
ourNewOutput.Value -= fakeChange.Value;
newTx.Outputs.Add(fakeChange);
isOurOutput.Add(fakeChange);
ctx.Logs.Write($"Added a fake change output of {fakeChange.Value} {network.CryptoCode} in the payjoin proposal");
}
}
}
@@ -434,7 +434,7 @@ namespace BTCPayServer.Payments.PayJoin
}
// The rest, we take from user's change
for (int i = 0; i < newTx.Outputs.Count && additionalFee > Money.Zero && allowedFeeBumpContribution > Money.Zero; i++)
for (int i = 0; i < newTx.Outputs.Count && additionalFee > Money.Zero && allowedSenderFeeContribution > Money.Zero; i++)
{
if (preferredFeeBumpOutput is TxOut &&
preferredFeeBumpOutput != newTx.Outputs[i])
@@ -444,10 +444,10 @@ namespace BTCPayServer.Payments.PayJoin
var outputContribution = Money.Min(additionalFee, newTx.Outputs[i].Value);
outputContribution = Money.Min(outputContribution,
newTx.Outputs[i].Value - newTx.Outputs[i].GetDustThreshold(minRelayTxFee));
outputContribution = Money.Min(outputContribution, allowedFeeBumpContribution);
outputContribution = Money.Min(outputContribution, allowedSenderFeeContribution);
newTx.Outputs[i].Value -= outputContribution;
additionalFee -= outputContribution;
allowedFeeBumpContribution -= outputContribution;
allowedSenderFeeContribution -= outputContribution;
}
}
@@ -457,12 +457,9 @@ namespace BTCPayServer.Payments.PayJoin
// we are not under the relay fee, it should be OK.
var newVSize = txBuilder.EstimateSize(newTx, true);
var newFeePaid = newTx.GetFee(txBuilder.FindSpentCoins(newTx));
if (new FeeRate(newFeePaid, newVSize) < minRelayTxFee)
if (new FeeRate(newFeePaid, newVSize) < (senderMinFeeRate ?? minRelayTxFee))
{
await UnlockUTXOs();
await BroadcastNow();
return UnprocessableEntity(CreatePayjoinError("not-enough-money",
"Not enough money is sent to pay for the additional payjoin inputs"));
return CreatePayjoinErrorAndLog(422, PayjoinReceiverWellknownErrors.NotEnoughMoney, "Not enough money is sent to pay for the additional payjoin inputs");
}
}
}
@@ -485,8 +482,8 @@ namespace BTCPayServer.Payments.PayJoin
// broadcast the payjoin.
var originalPaymentData = new BitcoinLikePaymentData(paymentAddress,
originalPaymentOutput.Value,
new OutPoint(originalTx.GetHash(), originalPaymentOutput.Index),
originalTx.RBF);
new OutPoint(ctx.OriginalTransaction.GetHash(), originalPaymentOutput.Index),
ctx.OriginalTransaction.RBF);
originalPaymentData.ConfirmationCount = -1;
originalPaymentData.PayjoinInformation = new PayjoinInformation()
{
@@ -497,25 +494,23 @@ namespace BTCPayServer.Payments.PayJoin
var payment = await _invoiceRepository.AddPayment(invoice.Id, DateTimeOffset.UtcNow, originalPaymentData, network, true);
if (payment is null)
{
await UnlockUTXOs();
await BroadcastNow();
return UnprocessableEntity(CreatePayjoinError("already-paid",
$"The original transaction has already been accounted"));
}
await _btcPayWalletProvider.GetWallet(network).SaveOffchainTransactionAsync(originalTx);
_eventAggregator.Publish(new InvoiceEvent(invoice, 1002, InvoiceEvent.ReceivedPayment) {Payment = payment});
await _btcPayWalletProvider.GetWallet(network).SaveOffchainTransactionAsync(ctx.OriginalTransaction);
_eventAggregator.Publish(new InvoiceEvent(invoice, 1002, InvoiceEvent.ReceivedPayment) { Payment = payment });
_eventAggregator.Publish(new UpdateTransactionLabel()
{
WalletId = new WalletId(invoice.StoreId, network.CryptoCode),
TransactionLabels = selectedUTXOs.GroupBy(pair => pair.Key.Hash ).Select(utxo =>
new KeyValuePair<uint256, List<(string color, string label)>>(utxo.Key,
new List<(string color, string label)>()
{
TransactionLabels = selectedUTXOs.GroupBy(pair => pair.Key.Hash).Select(utxo =>
new KeyValuePair<uint256, List<(string color, string label)>>(utxo.Key,
new List<(string color, string label)>()
{
UpdateTransactionLabel.PayjoinExposedLabelTemplate(invoice.Id)
}))
}))
.ToDictionary(pair => pair.Key, pair => pair.Value)
});
ctx.Success();
// BTCPay Server support PSBT set as hex
if (psbtFormat && HexEncoder.IsWellFormed(rawBody))
{
@@ -547,6 +542,21 @@ namespace BTCPayServer.Payments.PayJoin
return o;
}
private JObject CreatePayjoinError(PayjoinReceiverWellknownErrors error, string debug)
{
var o = new JObject();
o.Add(new JProperty("errorCode", PayjoinReceiverHelper.GetErrorCode(error)));
if (string.IsNullOrEmpty(debug) || !_env.IsDevelopping)
{
o.Add(new JProperty("message", PayjoinReceiverHelper.GetMessage(error)));
}
else
{
o.Add(new JProperty("message", debug));
}
return o;
}
public enum PayjoinUtxoSelectionType
{
Unavailable,
@@ -560,7 +570,7 @@ namespace BTCPayServer.Payments.PayJoin
if (availableUtxos.Length == 0)
return (Array.Empty<UTXO>(), PayjoinUtxoSelectionType.Unavailable);
// Assume the merchant wants to get rid of the dust
HashSet<OutPoint> locked = new HashSet<OutPoint>();
HashSet<OutPoint> locked = new HashSet<OutPoint>();
// We don't want to make too many db roundtrip which would be inconvenient for the sender
int maxTries = 30;
int currentTry = 0;
@@ -570,17 +580,17 @@ namespace BTCPayServer.Payments.PayJoin
//
// "UIH2": one input is larger than any output. This heuristically implies that no output is a payment, or, to say it better, it implies that this is not a normal wallet-created payment, it's something strange/exotic.
//src: https://gist.github.com/AdamISZ/4551b947789d3216bacfcb7af25e029e#gistcomment-2796539
foreach (var availableUtxo in availableUtxos)
{
if (currentTry >= maxTries)
break;
var invalid = false;
foreach (var input in otherInputs.Concat(new[] {availableUtxo.Value.GetValue(network)}))
foreach (var input in otherInputs.Concat(new[] { availableUtxo.Value.GetValue(network) }))
{
var computedOutputs =
otherOutputs.Concat(new[] {mainPaymentOutput + availableUtxo.Value.GetValue(network)});
otherOutputs.Concat(new[] { mainPaymentOutput + availableUtxo.Value.GetValue(network) });
if (computedOutputs.Any(output => input > output))
{
//UIH 1 & 2
@@ -595,7 +605,7 @@ namespace BTCPayServer.Payments.PayJoin
}
if (await _payJoinRepository.TryLock(availableUtxo.Outpoint))
{
return (new[] {availableUtxo}, PayjoinUtxoSelectionType.HeuristicBased);
return (new[] { availableUtxo }, PayjoinUtxoSelectionType.HeuristicBased);
}
locked.Add(availableUtxo.Outpoint);
@@ -607,7 +617,7 @@ namespace BTCPayServer.Payments.PayJoin
break;
if (await _payJoinRepository.TryLock(utxo.Outpoint))
{
return (new[] {utxo}, PayjoinUtxoSelectionType.Ordered);
return (new[] { utxo }, PayjoinUtxoSelectionType.Ordered);
}
currentTry++;
}

View File

@@ -0,0 +1,69 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Logging;
using BTCPayServer.Services.Invoices;
using Microsoft.Extensions.Logging;
using NBitcoin;
using NBitcoin.JsonConverters;
using NBitpayClient;
using NBXplorer;
namespace BTCPayServer.Payments.PayJoin
{
public class PayjoinReceiverContext
{
private readonly InvoiceRepository _invoiceRepository;
private readonly ExplorerClient _explorerClient;
private readonly PayJoinRepository _payJoinRepository;
public PayjoinReceiverContext(InvoiceRepository invoiceRepository, ExplorerClient explorerClient, PayJoinRepository payJoinRepository)
{
_invoiceRepository = invoiceRepository;
_explorerClient = explorerClient;
_payJoinRepository = payJoinRepository;
}
public Invoice Invoice { get; set; }
public NBitcoin.Transaction OriginalTransaction { get; set; }
public InvoiceLogs Logs { get; } = new InvoiceLogs();
public OutPoint[] LockedUTXOs { get; set; }
public async Task DisposeAsync()
{
List<Task> disposing = new List<Task>();
if (Invoice != null)
{
disposing.Add(_invoiceRepository.AddInvoiceLogs(Invoice.Id, Logs));
}
if (!doNotBroadcast && OriginalTransaction != null)
{
disposing.Add(_explorerClient.BroadcastAsync(OriginalTransaction));
}
if (!success && LockedUTXOs != null)
{
disposing.Add(_payJoinRepository.TryUnlock(LockedUTXOs));
}
try
{
await Task.WhenAll(disposing);
}
catch (Exception ex)
{
BTCPayServer.Logging.Logs.PayServer.LogWarning(ex, "Error while disposing the PayjoinReceiverContext");
}
}
bool doNotBroadcast = false;
public void DoNotBroadcast()
{
doNotBroadcast = true;
}
bool success = false;
public void Success()
{
doNotBroadcast = true;
success = true;
}
}
}

View File

@@ -1,11 +1,13 @@
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Payments.Changelly.Models;
using Google.Apis.Http;
using NBitcoin;
using Newtonsoft.Json;
@@ -43,8 +45,9 @@ namespace BTCPayServer.Services
public class PayjoinClientParameters
{
public Money MaxFeeBumpContribution { get; set; }
public int? FeeBumpIndex { get; set; }
public Money MaxAdditionalFeeContribution { get; set; }
public FeeRate MinFeeRate { get; set; }
public int? AdditionalFeeOutputIndex { get; set; }
public int Version { get; set; } = 1;
}
@@ -72,6 +75,7 @@ namespace BTCPayServer.Services
}
public Money MaxFeeBumpContribution { get; set; }
public FeeRate MinimumFeeRate { get; set; }
public async Task<PSBT> RequestPayjoin(Uri endpoint, DerivationSchemeSettings derivationSchemeSettings,
PSBT originalTx, CancellationToken cancellationToken)
@@ -94,7 +98,7 @@ namespace BTCPayServer.Services
var changeOutput = originalTx.Outputs.CoinsFor(derivationSchemeSettings.AccountDerivation, signingAccount.AccountKey, signingAccount.GetRootedKeyPath())
.FirstOrDefault();
if (changeOutput is PSBTOutput o)
clientParameters.FeeBumpIndex = (int)o.Index;
clientParameters.AdditionalFeeOutputIndex = (int)o.Index;
var sentBefore = -originalTx.GetBalance(derivationSchemeSettings.AccountDerivation,
signingAccount.AccountKey,
signingAccount.GetRootedKeyPath());
@@ -102,8 +106,9 @@ namespace BTCPayServer.Services
if (!originalTx.TryGetEstimatedFeeRate(out var originalFeeRate) || !originalTx.TryGetVirtualSize(out var oldVirtualSize))
throw new ArgumentException("originalTx should have utxo information", nameof(originalTx));
var originalFee = originalTx.GetFee();
clientParameters.MaxFeeBumpContribution = MaxFeeBumpContribution is null ? originalFee : MaxFeeBumpContribution;
clientParameters.MaxAdditionalFeeContribution = MaxFeeBumpContribution is null ? originalFee : MaxFeeBumpContribution;
if (MinimumFeeRate is FeeRate v)
clientParameters.MinFeeRate = v;
var cloned = originalTx.Clone();
cloned.Finalize();
@@ -220,20 +225,25 @@ namespace BTCPayServer.Services
if (ourInputCount < originalTx.Inputs.Count)
throw new PayjoinSenderException("The payjoin receiver removed some of our inputs");
if (!newPSBT.TryGetEstimatedFeeRate(out var newFeeRate) || !newPSBT.TryGetVirtualSize(out var newVirtualSize))
throw new PayjoinSenderException("The payjoin receiver did not included UTXO information to calculate fee correctly");
if (clientParameters.MinFeeRate is FeeRate minFeeRate)
{
if (newFeeRate < minFeeRate)
throw new PayjoinSenderException("The payjoin receiver created a payjoin with a too low fee rate");
}
var sentAfter = -newPSBT.GetBalance(derivationSchemeSettings.AccountDerivation,
signingAccount.AccountKey,
signingAccount.GetRootedKeyPath());
if (sentAfter > sentBefore)
{
var overPaying = sentAfter - sentBefore;
if (!newPSBT.TryGetEstimatedFeeRate(out var newFeeRate) || !newPSBT.TryGetVirtualSize(out var newVirtualSize))
throw new PayjoinSenderException("The payjoin receiver did not included UTXO information to calculate fee correctly");
var additionalFee = newPSBT.GetFee() - originalFee;
if (overPaying > additionalFee)
throw new PayjoinSenderException("The payjoin receiver is sending more money to himself");
if (overPaying > clientParameters.MaxFeeBumpContribution)
if (overPaying > clientParameters.MaxAdditionalFeeContribution)
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
@@ -255,10 +265,12 @@ namespace BTCPayServer.Services
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}");
if (clientParameters.AdditionalFeeOutputIndex is int additionalFeeOutputIndex)
parameters.Add($"additionalfeeoutputindex={additionalFeeOutputIndex.ToString(CultureInfo.InvariantCulture)}");
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;
}
@@ -291,55 +303,61 @@ namespace BTCPayServer.Services
NeedUTXOInformation,
InvalidTransaction
}
public class PayjoinReceiverHelper
{
static IEnumerable<(PayjoinReceiverWellknownErrors EnumValue, string ErrorCode, string Message)> Get()
{
yield return (PayjoinReceiverWellknownErrors.LeakingData, "leaking-data", "Key path information or GlobalXPubs should not be included in the original PSBT.");
yield return (PayjoinReceiverWellknownErrors.PSBTNotFinalized, "psbt-not-finalized", "The original PSBT must be finalized.");
yield return (PayjoinReceiverWellknownErrors.Unavailable, "unavailable", "The payjoin endpoint is not available for now.");
yield return (PayjoinReceiverWellknownErrors.NotEnoughMoney, "not-enough-money", "The receiver added some inputs but could not bump the fee of the payjoin proposal.");
yield return (PayjoinReceiverWellknownErrors.InsanePSBT, "insane-psbt", "Some consistency check on the PSBT failed.");
yield return (PayjoinReceiverWellknownErrors.VersionUnsupported, "version-unsupported", "This version of payjoin is not supported.");
yield return (PayjoinReceiverWellknownErrors.NeedUTXOInformation, "need-utxo-information", "The witness UTXO or non witness UTXO is missing.");
yield return (PayjoinReceiverWellknownErrors.InvalidTransaction, "invalid-transaction", "The original transaction is invalid for payjoin");
}
public static string GetErrorCode(PayjoinReceiverWellknownErrors err)
{
return Get().Single(o => o.EnumValue == err).ErrorCode;
}
public static PayjoinReceiverWellknownErrors? GetWellknownError(string errorCode)
{
var t = Get().FirstOrDefault(o => o.ErrorCode == errorCode);
if (t == default)
return null;
return t.EnumValue;
}
static string UnknownError = "Unknown error from the receiver";
public static string GetMessage(string errorCode)
{
return Get().FirstOrDefault(o => o.ErrorCode == errorCode).Message ?? UnknownError;
}
public static string GetMessage(PayjoinReceiverWellknownErrors err)
{
return Get().Single(o => o.EnumValue == err).Message;
}
}
public class PayjoinReceiverException : PayjoinException
{
public PayjoinReceiverException(string errorCode, string receiverDebugMessage) : base(FormatMessage(errorCode, receiverDebugMessage))
public PayjoinReceiverException(string errorCode, string receiverMessage) : base(FormatMessage(errorCode))
{
ErrorCode = errorCode;
ReceiverDebugMessage = receiverDebugMessage;
WellknownError = errorCode switch
{
"leaking-data" => PayjoinReceiverWellknownErrors.LeakingData,
"psbt-not-finalized" => PayjoinReceiverWellknownErrors.PSBTNotFinalized,
"unavailable" => PayjoinReceiverWellknownErrors.Unavailable,
"out-of-utxos" => PayjoinReceiverWellknownErrors.OutOfUTXOS,
"not-enough-money" => PayjoinReceiverWellknownErrors.NotEnoughMoney,
"insane-psbt" => PayjoinReceiverWellknownErrors.InsanePSBT,
"version-unsupported" => PayjoinReceiverWellknownErrors.VersionUnsupported,
"need-utxo-information" => PayjoinReceiverWellknownErrors.NeedUTXOInformation,
"invalid-transaction" => PayjoinReceiverWellknownErrors.InvalidTransaction,
_ => null
};
ReceiverMessage = receiverMessage;
WellknownError = PayjoinReceiverHelper.GetWellknownError(errorCode);
ErrorMessage = PayjoinReceiverHelper.GetMessage(errorCode);
}
public string ErrorCode { get; }
public string ErrorMessage { get; }
public string ReceiverDebugMessage { get; }
public string ReceiverMessage { get; }
public PayjoinReceiverWellknownErrors? WellknownError
{
get;
}
private static string FormatMessage(string errorCode, string receiverDebugMessage)
private static string FormatMessage(string errorCode)
{
return $"{errorCode}: {GetMessage(errorCode)}";
}
private static string GetMessage(string errorCode)
{
return errorCode switch
{
"leaking-data" => "Key path information or GlobalXPubs should not be included in the original PSBT.",
"psbt-not-finalized" => "The original PSBT must be finalized.",
"unavailable" => "The payjoin endpoint is not available for now.",
"out-of-utxos" => "The receiver does not have any UTXO to contribute in a 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.",
"version-unsupported" => "This version of payjoin is not supported.",
"need-utxo-information" => "The witness UTXO or non witness UTXO is missing",
"invalid-transaction" => "The original transaction is invalid for payjoin",
_ => "Unknown error"
};
return $"{errorCode}: {PayjoinReceiverHelper.GetMessage(errorCode)}";
}
}