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 ;
2020-04-07 18:14:31 +09:00
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
}
2020-04-05 22:44:34 +09: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 ) ) )
2020-04-06 20:27:48 +09:00
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 > ( ) ;
2020-04-05 22:44:34 +09:00
async Task UnlockUTXOs ( )
{
await _payJoinRepository . TryUnlock ( selectedUTXOs . Select ( o = > o . Key ) . ToArray ( ) ) ;
}
2020-04-07 18:14:31 +09:00
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
2020-04-07 18:14:31 +09: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
{
2020-04-05 22:44:34 +09: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
}
2020-04-07 18:14:31 +09:00
var originalPaymentValue = originalPaymentOutput . Value ;
2020-04-05 22:44:34 +09:00
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
2020-04-05 22:44:34 +09:00
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
}
2020-04-07 18:14:31 +09:00
Money contributedAmount = Money . Zero ;
2020-03-30 00:28:22 +09:00
var newTx = originalTx . Clone ( ) ;
2020-04-07 18:14:31 +09:00
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
{
2020-04-07 18:14:31 +09: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
}
2020-04-07 18:14:31 +09:00
ourNewOutput . Value + = contributedAmount ;
var minRelayTxFee = this . _dashboard . Get ( network . CryptoCode ) . Status . BitcoinStatus ? . MinRelayTxFee ? ?
new FeeRate ( 1.0 m ) ;
// 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
{
2020-04-07 20:04:08 +09:00
// If the user overpaid, taking fee on our output (useful if sender dump a full UTXO for privacy)
2020-04-07 18:14:31 +09:00
for ( int i = 0 ; i < newTx . Outputs . Count & & additionalFee > Money . Zero & & due < Money . Zero ; i + + )
2020-01-06 13:57:32 +01:00
{
2020-04-07 18:14:31 +09: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 ;
2020-04-07 20:04:08 +09:00
due + = outputContribution ;
2020-04-07 18:14:31 +09:00
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
2020-04-07 18:14:31 +09:00
for ( int i = 0 ; i < newTx . Outputs . Count & & additionalFee > Money . Zero ; i + + )
2020-03-05 19:04:08 +01:00
{
2020-04-07 18:14:31 +09:00
if ( ! isOurOutput . Contains ( newTx . Outputs [ i ] ) )
2020-03-30 00:28:22 +09:00
{
2020-04-07 18:14:31 +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 )
{
2020-04-05 22:44:34 +09:00
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 ;
}
2020-04-05 22:44:34 +09:00
// 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 ,
2020-04-07 18:14:31 +09:00
originalPaymentOutput . Value ,
new OutPoint ( originalTx . GetHash ( ) , originalPaymentOutput . Index ) ,
2020-03-30 00:28:22 +09:00
originalTx . RBF ) ;
2020-04-05 22:44:34 +09:00
originalPaymentData . ConfirmationCount = - 1 ;
originalPaymentData . PayjoinInformation = new PayjoinInformation ( )
2020-01-06 13:57:32 +01:00
{
2020-04-05 22:44:34 +09: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
} ;
2020-04-05 22:44:34 +09: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
}
}
}