btcpayserver/BTCPayServer/Payments/PayJoin/PayJoinEndpointController.cs

467 lines
22 KiB
C#
Raw Normal View History

2020-01-06 13:57:32 +01:00
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Data;
2020-03-30 00:28:22 +09:00
using BTCPayServer.Events;
2020-01-06 13:57:32 +01:00
using BTCPayServer.Filters;
2020-03-30 00:28:22 +09:00
using BTCPayServer.HostedServices;
2020-01-06 13:57:32 +01:00
using BTCPayServer.Payments.Bitcoin;
2020-03-30 00:28:22 +09:00
using BTCPayServer.Services;
2020-01-06 13:57:32 +01:00
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Wallets;
using Microsoft.AspNetCore.Cors;
2020-03-30 00:28:22 +09:00
using Microsoft.AspNetCore.Http;
2020-01-06 13:57:32 +01:00
using Microsoft.AspNetCore.Mvc;
2020-03-30 00:28:22 +09:00
using Microsoft.AspNetCore.Mvc.ModelBinding;
2020-01-06 13:57:32 +01:00
using NBitcoin;
using NBitcoin.DataEncoders;
2020-03-30 00:28:22 +09:00
using NBitcoin.Logging;
2020-01-06 13:57:32 +01:00
using NBXplorer;
using NBXplorer.Models;
2020-03-30 00:28:22 +09:00
using Newtonsoft.Json.Linq;
2020-01-06 13:57:32 +01:00
using NicolasDorier.RateLimits;
2020-03-30 00:28:22 +09:00
using Microsoft.Extensions.Logging;
using NBXplorer.DerivationStrategy;
2020-01-06 13:57:32 +01:00
namespace BTCPayServer.Payments.PayJoin
{
[Route("{cryptoCode}/bpu")]
public class PayJoinEndpointController : ControllerBase
{
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
private readonly InvoiceRepository _invoiceRepository;
private readonly ExplorerClientProvider _explorerClientProvider;
private readonly StoreRepository _storeRepository;
private readonly BTCPayWalletProvider _btcPayWalletProvider;
2020-03-30 00:28:22 +09:00
private readonly PayJoinRepository _payJoinRepository;
private readonly EventAggregator _eventAggregator;
private readonly NBXplorerDashboard _dashboard;
private readonly DelayedTransactionBroadcaster _broadcaster;
2020-01-06 13:57:32 +01:00
public PayJoinEndpointController(BTCPayNetworkProvider btcPayNetworkProvider,
InvoiceRepository invoiceRepository, ExplorerClientProvider explorerClientProvider,
StoreRepository storeRepository, BTCPayWalletProvider btcPayWalletProvider,
2020-03-30 00:28:22 +09:00
PayJoinRepository payJoinRepository,
EventAggregator eventAggregator,
NBXplorerDashboard dashboard,
DelayedTransactionBroadcaster broadcaster)
2020-01-06 13:57:32 +01:00
{
_btcPayNetworkProvider = btcPayNetworkProvider;
_invoiceRepository = invoiceRepository;
_explorerClientProvider = explorerClientProvider;
_storeRepository = storeRepository;
_btcPayWalletProvider = btcPayWalletProvider;
2020-03-30 00:28:22 +09:00
_payJoinRepository = payJoinRepository;
_eventAggregator = eventAggregator;
_dashboard = dashboard;
_broadcaster = broadcaster;
2020-01-06 13:57:32 +01:00
}
2020-03-30 00:28:22 +09:00
[HttpPost("")]
2020-01-06 13:57:32 +01:00
[IgnoreAntiforgeryToken]
[EnableCors(CorsPolicies.All)]
[MediaTypeConstraint("text/plain")]
[RateLimitsFilter(ZoneLimits.PayJoin, Scope = RateLimitsScope.RemoteAddress)]
2020-03-30 00:28:22 +09:00
public async Task<IActionResult> Submit(string cryptoCode)
2020-01-06 13:57:32 +01:00
{
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
if (network == null)
{
2020-03-30 00:28:22 +09:00
return BadRequest(CreatePayjoinError(400, "invalid-network", "Incorrect network"));
2020-01-06 13:57:32 +01:00
}
2020-03-30 00:28:22 +09:00
var explorer = _explorerClientProvider.GetExplorerClient(network);
if (Request.ContentLength is long length)
2020-01-06 13:57:32 +01:00
{
2020-03-30 00:28:22 +09:00
if (length > 1_000_000)
return this.StatusCode(413,
CreatePayjoinError(413, "payload-too-large", "The transaction is too big to be processed"));
2020-01-06 13:57:32 +01:00
}
2020-03-30 00:28:22 +09:00
else
2020-01-06 13:57:32 +01:00
{
2020-03-30 00:28:22 +09:00
return StatusCode(411,
CreatePayjoinError(411, "missing-content-length",
"The http header Content-Length should be filled"));
2020-01-06 13:57:32 +01:00
}
2020-03-30 00:28:22 +09:00
string rawBody;
using (StreamReader reader = new StreamReader(Request.Body, Encoding.UTF8))
2020-01-06 13:57:32 +01:00
{
2020-03-30 00:28:22 +09:00
rawBody = (await reader.ReadToEndAsync()) ?? string.Empty;
2020-01-06 13:57:32 +01:00
}
2020-03-30 00:28:22 +09:00
Transaction originalTx = null;
FeeRate originalFeeRate = null;
bool psbtFormat = true;
if (!PSBT.TryParse(rawBody, network.NBitcoinNetwork, out var psbt))
2020-01-06 13:57:32 +01:00
{
2020-03-30 00:28:22 +09:00
psbtFormat = false;
if (!Transaction.TryParse(rawBody, network.NBitcoinNetwork, out var tx))
return BadRequest(CreatePayjoinError(400, "invalid-format", "invalid transaction or psbt"));
originalTx = 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++)
2020-03-27 14:58:01 +01:00
{
2020-03-30 00:28:22 +09:00
psbt.Inputs[i].FinalScriptSig = tx.Inputs[i].ScriptSig;
psbt.Inputs[i].FinalScriptWitness = tx.Inputs[i].WitScript;
2020-03-27 14:58:01 +01:00
}
2020-01-06 13:57:32 +01:00
}
2020-03-30 00:28:22 +09:00
else
2020-01-06 13:57:32 +01:00
{
2020-03-30 00:28:22 +09:00
if (!psbt.IsAllFinalized())
return BadRequest(CreatePayjoinError(400, "psbt-not-finalized", "The PSBT should be finalized"));
originalTx = psbt.ExtractTransaction();
2020-01-06 13:57:32 +01:00
}
async Task BroadcastNow()
{
await _explorerClientProvider.GetExplorerClient(network).BroadcastAsync(originalTx);
}
2020-03-30 00:28:22 +09:00
if (originalTx.Inputs.Any(i => !(i.GetSigner() is WitKeyId)))
return BadRequest(CreatePayjoinError(400, "unsupported-inputs", "Payjoin only support P2WPKH inputs"));
2020-03-30 00:28:22 +09:00
if (psbt.CheckSanity() is var errors && errors.Count != 0)
2020-01-06 13:57:32 +01:00
{
2020-03-30 00:28:22 +09:00
return BadRequest(CreatePayjoinError(400, "insane-psbt", $"This PSBT is insane ({errors[0]})"));
2020-01-06 13:57:32 +01:00
}
2020-03-30 00:28:22 +09:00
if (!psbt.TryGetEstimatedFeeRate(out originalFeeRate))
2020-01-06 13:57:32 +01:00
{
2020-03-30 00:28:22 +09:00
return BadRequest(CreatePayjoinError(400, "need-utxo-information",
"You need to provide Witness UTXO information to the PSBT."));
2020-01-06 13:57:32 +01:00
}
2020-03-30 00:28:22 +09:00
// This is actually not a mandatory check, but we don't want implementers
// to leak global xpubs
if (psbt.GlobalXPubs.Any())
2020-01-06 13:57:32 +01:00
{
2020-03-30 00:28:22 +09:00
return BadRequest(CreatePayjoinError(400, "leaking-data",
"GlobalXPubs should not be included in the PSBT"));
2020-01-06 13:57:32 +01:00
}
2020-03-30 00:28:22 +09:00
if (psbt.Outputs.Any(o => o.HDKeyPaths.Count != 0) || psbt.Inputs.Any(o => o.HDKeyPaths.Count != 0))
2020-01-06 13:57:32 +01:00
{
2020-03-30 00:28:22 +09:00
return BadRequest(CreatePayjoinError(400, "leaking-data",
"Keypath information should not be included in the PSBT"));
2020-01-06 13:57:32 +01:00
}
2020-03-30 00:28:22 +09:00
if (psbt.Inputs.Any(o => !o.IsFinalized()))
2020-03-05 19:04:08 +01:00
{
2020-03-30 00:28:22 +09:00
return BadRequest(CreatePayjoinError(400, "psbt-not-finalized", "The PSBT Should be finalized"));
2020-03-05 19:04:08 +01:00
}
2020-03-30 00:28:22 +09:00
////////////
2020-03-05 19:04:08 +01:00
2020-03-30 00:28:22 +09:00
var mempool = await explorer.BroadcastAsync(originalTx, true);
if (!mempool.Success)
2020-01-06 13:57:32 +01:00
{
2020-03-30 00:28:22 +09:00
return BadRequest(CreatePayjoinError(400, "invalid-transaction",
$"Provided transaction isn't mempool eligible {mempool.RPCCodeMessage}"));
2020-01-06 13:57:32 +01:00
}
2020-03-30 00:28:22 +09:00
var paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike);
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;
2020-03-30 00:28:22 +09:00
BitcoinAddress paymentAddress = null;
InvoiceEntity invoice = null;
DerivationSchemeSettings derivationSchemeSettings = null;
foreach (var output in psbt.Outputs)
2020-01-06 13:57:32 +01:00
{
2020-03-30 00:28:22 +09:00
var key = output.ScriptPubKey.Hash + "#" + network.CryptoCode.ToUpperInvariant();
invoice = (await _invoiceRepository.GetInvoicesFromAddresses(new[] {key})).FirstOrDefault();
if (invoice is null)
continue;
derivationSchemeSettings = invoice.GetSupportedPaymentMethod<DerivationSchemeSettings>(paymentMethodId)
.SingleOrDefault();
if (derivationSchemeSettings is null)
continue;
var paymentMethod = invoice.GetPaymentMethod(paymentMethodId);
var paymentDetails =
paymentMethod.GetPaymentMethodDetails() as Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod;
if (paymentDetails is null || !paymentDetails.PayjoinEnabled)
continue;
if (invoice.GetAllBitcoinPaymentData().Any())
{
return UnprocessableEntity(CreatePayjoinError(422, "already-paid",
$"The invoice this PSBT is paying has already been partially or completely paid"));
}
2020-01-06 13:57:32 +01:00
2020-03-30 00:28:22 +09:00
paidSomething = true;
due = paymentMethod.Calculate().TotalDue - output.Value;
if (due > Money.Zero)
{
break;
}
2020-01-06 13:57:32 +01:00
2020-03-30 00:28:22 +09:00
if (!await _payJoinRepository.TryLockInputs(originalTx.Inputs.Select(i => i.PrevOut).ToArray()))
{
return BadRequest(CreatePayjoinError(400, "inputs-already-used",
"Some of those inputs have already been used to make payjoin transaction"));
}
2020-01-06 13:57:32 +01:00
2020-03-30 00:28:22 +09:00
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();
utxos = utxos.Where(u => !prevOuts.Contains(u.Outpoint)).ToArray();
foreach (var utxo in await SelectUTXO(network, utxos, output.Value,
psbt.Outputs.Where(o => o.Index != output.Index).Select(o => o.Value).ToArray()))
{
selectedUTXOs.Add(utxo.Outpoint, utxo);
}
2020-01-06 13:57:32 +01:00
originalPaymentOutput = output;
2020-03-30 00:28:22 +09:00
paymentAddress = paymentDetails.GetDepositAddress(network.NBitcoinNetwork);
break;
2020-01-06 13:57:32 +01:00
}
2020-03-30 00:28:22 +09:00
if (!paidSomething)
2020-01-06 13:57:32 +01:00
{
2020-03-30 00:28:22 +09:00
return BadRequest(CreatePayjoinError(400, "invoice-not-found",
"This transaction does not pay any invoice with payjoin"));
2020-01-06 13:57:32 +01:00
}
2020-03-30 00:28:22 +09:00
if (due is null || due > Money.Zero)
2020-01-06 13:57:32 +01:00
{
2020-03-30 00:28:22 +09:00
return BadRequest(CreatePayjoinError(400, "invoice-not-fully-paid",
"The transaction must pay the whole invoice"));
2020-01-06 13:57:32 +01:00
}
2020-03-30 00:28:22 +09:00
if (selectedUTXOs.Count == 0)
2020-01-06 13:57:32 +01:00
{
await BroadcastNow();
2020-03-30 00:28:22 +09:00
return StatusCode(503,
CreatePayjoinError(503, "out-of-utxos",
"We do not have any UTXO available for making a payjoin for now"));
2020-01-06 13:57:32 +01:00
}
var originalPaymentValue = originalPaymentOutput.Value;
await _broadcaster.Schedule(DateTimeOffset.UtcNow + TimeSpan.FromMinutes(1.0), originalTx, network);
2020-01-06 13:57:32 +01:00
2020-03-30 00:28:22 +09:00
//check if wallet of store is configured to be hot wallet
var extKeyStr = await explorer.GetMetadataAsync<string>(
derivationSchemeSettings.AccountDerivation,
WellknownMetadataKeys.AccountHDKey);
if (extKeyStr == null)
2020-01-06 13:57:32 +01:00
{
2020-03-30 00:28:22 +09:00
// This should not happen, as we check the existance of private key before creating invoice with payjoin
await UnlockUTXOs();
await BroadcastNow();
2020-03-30 00:28:22 +09:00
return StatusCode(500, CreatePayjoinError(500, "unavailable", $"This service is unavailable for now"));
2020-01-06 13:57:32 +01:00
}
Money contributedAmount = Money.Zero;
2020-03-30 00:28:22 +09:00
var newTx = originalTx.Clone();
var ourNewOutput = newTx.Outputs[originalPaymentOutput.Index];
HashSet<TxOut> isOurOutput = new HashSet<TxOut>();
isOurOutput.Add(ourNewOutput);
2020-03-30 00:28:22 +09:00
foreach (var selectedUTXO in selectedUTXOs.Select(o => o.Value))
2020-01-06 13:57:32 +01:00
{
contributedAmount += (Money)selectedUTXO.Value;
2020-03-30 00:28:22 +09:00
newTx.Inputs.Add(selectedUTXO.Outpoint);
2020-01-06 13:57:32 +01:00
}
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)
{
var change = await explorer.GetUnusedAsync(derivationSchemeSettings.AccountDerivation, DerivationFeature.Change);
var randomChangeAmount = RandomUtils.GetUInt64() % (ulong)contributedAmount.Satoshi;
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);
}
}
2020-01-06 13:57:32 +01:00
2020-03-30 00:28:22 +09:00
var rand = new Random();
Utils.Shuffle(newTx.Inputs, rand);
Utils.Shuffle(newTx.Outputs, rand);
2020-01-06 13:57:32 +01:00
2020-03-30 00:28:22 +09:00
// Remove old signatures as they are not valid anymore
foreach (var input in newTx.Inputs)
2020-01-06 13:57:32 +01:00
{
2020-03-30 00:28:22 +09:00
input.WitScript = WitScript.Empty;
2020-01-06 13:57:32 +01:00
}
2020-03-30 00:28:22 +09:00
Money ourFeeContribution = Money.Zero;
// We need to adjust the fee to keep a constant fee rate
var txBuilder = network.NBitcoinNetwork.CreateTransactionBuilder();
txBuilder.AddCoins(psbt.Inputs.Select(i => i.GetCoin()));
txBuilder.AddCoins(selectedUTXOs.Select(o => o.Value.AsCoin()));
Money expectedFee = txBuilder.EstimateFees(newTx, originalFeeRate);
Money actualFee = newTx.GetFee(txBuilder.FindSpentCoins(newTx));
Money additionalFee = expectedFee - actualFee;
if (additionalFee > Money.Zero)
2020-01-06 13:57:32 +01:00
{
// 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++)
2020-01-06 13:57:32 +01:00
{
if (isOurOutput.Contains(newTx.Outputs[i]))
{
var outputContribution = Money.Min(additionalFee, -due);
outputContribution = Money.Min(outputContribution,
newTx.Outputs[i].Value - newTx.Outputs[i].GetDustThreshold(minRelayTxFee));
newTx.Outputs[i].Value -= outputContribution;
additionalFee -= outputContribution;
due += outputContribution;
ourFeeContribution += outputContribution;
}
2020-03-30 00:28:22 +09:00
}
2020-01-06 13:57:32 +01:00
2020-03-30 00:28:22 +09:00
// The rest, we take from user's change
for (int i = 0; i < newTx.Outputs.Count && additionalFee > Money.Zero; i++)
2020-03-05 19:04:08 +01:00
{
if (!isOurOutput.Contains(newTx.Outputs[i]))
2020-03-30 00:28:22 +09:00
{
var outputContribution = Money.Min(additionalFee, newTx.Outputs[i].Value);
outputContribution = Money.Min(outputContribution,
newTx.Outputs[i].Value - newTx.Outputs[i].GetDustThreshold(minRelayTxFee));
newTx.Outputs[i].Value -= outputContribution;
additionalFee -= outputContribution;
2020-03-30 00:28:22 +09:00
}
2020-03-05 19:04:08 +01:00
}
2020-03-30 00:28:22 +09:00
if (additionalFee > Money.Zero)
2020-01-06 13:57:32 +01:00
{
2020-03-30 00:28:22 +09:00
// We could not pay fully the additional fee, however, as long as
// 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)
{
await UnlockUTXOs();
await BroadcastNow();
2020-03-30 00:28:22 +09:00
return UnprocessableEntity(CreatePayjoinError(422, "not-enough-money",
"Not enough money is sent to pay for the additional payjoin inputs"));
}
2020-01-06 13:57:32 +01:00
}
}
2020-03-30 00:28:22 +09:00
var accountKey = ExtKey.Parse(extKeyStr, network.NBitcoinNetwork);
var newPsbt = PSBT.FromTransaction(newTx, network.NBitcoinNetwork);
foreach (var selectedUtxo in selectedUTXOs.Select(o => o.Value))
2020-01-06 13:57:32 +01:00
{
2020-03-30 00:28:22 +09:00
var signedInput = newPsbt.Inputs.FindIndexedInput(selectedUtxo.Outpoint);
signedInput.UpdateFromCoin(selectedUtxo.AsCoin());
var privateKey = accountKey.Derive(selectedUtxo.KeyPath).PrivateKey;
signedInput.Sign(privateKey);
signedInput.FinalizeInput();
newTx.Inputs[signedInput.Index].WitScript = newPsbt.Inputs[(int)signedInput.Index].FinalScriptWitness;
}
// Add the transaction to the payments with a confirmation of -1.
// This will make the invoice paid even if the user do not
// broadcast the payjoin.
var originalPaymentData = new BitcoinLikePaymentData(paymentAddress,
originalPaymentOutput.Value,
new OutPoint(originalTx.GetHash(), originalPaymentOutput.Index),
2020-03-30 00:28:22 +09:00
originalTx.RBF);
originalPaymentData.ConfirmationCount = -1;
originalPaymentData.PayjoinInformation = new PayjoinInformation()
2020-01-06 13:57:32 +01:00
{
CoinjoinTransactionHash = newPsbt.GetGlobalTransaction().GetHash(),
CoinjoinValue = originalPaymentValue - ourFeeContribution,
2020-03-30 00:28:22 +09:00
ContributedOutPoints = selectedUTXOs.Select(o => o.Key).ToArray()
2020-01-06 13:57:32 +01:00
};
var payment = await _invoiceRepository.AddPayment(invoice.Id, DateTimeOffset.UtcNow, originalPaymentData, network, true);
if (payment is null)
{
await UnlockUTXOs();
await BroadcastNow();
return UnprocessableEntity(CreatePayjoinError(422, "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});
2020-03-30 00:28:22 +09:00
if (psbtFormat)
return Ok(newPsbt.ToBase64());
else
return Ok(newTx.ToHex());
}
private JObject CreatePayjoinError(int httpCode, string errorCode, string friendlyMessage)
{
var o = new JObject();
o.Add(new JProperty("httpCode", httpCode));
o.Add(new JProperty("errorCode", errorCode));
o.Add(new JProperty("message", friendlyMessage));
return o;
2020-01-06 13:57:32 +01:00
}
2020-03-30 00:28:22 +09:00
private async Task<UTXO[]> SelectUTXO(BTCPayNetwork network, UTXO[] availableUtxos, Money paymentAmount,
Money[] otherOutputs)
2020-01-06 13:57:32 +01:00
{
2020-03-30 00:28:22 +09:00
if (availableUtxos.Length == 0)
return Array.Empty<UTXO>();
// Assume the merchant wants to get rid of the dust
Utils.Shuffle(availableUtxos);
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;
List<UTXO> utxosByPriority = new List<UTXO>();
2020-01-06 13:57:32 +01:00
// UIH = "unnecessary input heuristic", basically "a wallet wouldn't choose more utxos to spend in this scenario".
//
// "UIH1" : one output is smaller than any input. This heuristically implies that that output is not a payment, and must therefore be a change output.
//
// "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)
{
2020-03-30 00:28:22 +09:00
if (currentTry >= maxTries)
break;
2020-01-06 13:57:32 +01:00
//we can only check against our input as we dont know the value of the rest.
2020-03-30 00:28:22 +09:00
var input = (Money)availableUtxo.Value;
2020-01-06 13:57:32 +01:00
var paymentAmountSum = input + paymentAmount;
if (otherOutputs.Concat(new[] {paymentAmountSum}).Any(output => input > output))
{
//UIH 1 & 2
continue;
}
2020-03-30 00:28:22 +09:00
if (await _payJoinRepository.TryLock(availableUtxo.Outpoint))
{
return new UTXO[] { availableUtxo };
}
locked.Add(availableUtxo.Outpoint);
currentTry++;
2020-01-06 13:57:32 +01:00
}
2020-03-30 00:28:22 +09:00
foreach (var utxo in availableUtxos.Where(u => !locked.Contains(u.Outpoint)))
{
if (currentTry >= maxTries)
break;
if (await _payJoinRepository.TryLock(utxo.Outpoint))
{
return new UTXO[] { utxo };
}
currentTry++;
}
return Array.Empty<UTXO>();
2020-01-06 13:57:32 +01:00
}
}
}