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 ;
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 ;
using Microsoft.AspNetCore.Mvc ;
using NBitcoin ;
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-04-07 18:14:31 +09:00
using NBXplorer.DerivationStrategy ;
2020-04-08 13:46:11 +09:00
using System.Diagnostics.CodeAnalysis ;
2020-04-23 15:02:00 +02:00
using NBitcoin.DataEncoders ;
2020-01-06 13:57:32 +01:00
namespace BTCPayServer.Payments.PayJoin
{
2020-04-07 14:18:56 +02:00
[Route("{cryptoCode}/" + PayjoinClient.BIP21EndpointKey)]
2020-01-06 13:57:32 +01:00
public class PayJoinEndpointController : ControllerBase
{
2020-04-08 13:46:11 +09:00
/// <summary>
/// This comparer sorts utxo in a deterministic manner
/// based on a random parameter.
/// When a UTXO is locked because used in a coinjoin, in might be unlocked
/// later if the coinjoin failed.
/// Such UTXO should be reselected in priority so we don't expose the other UTXOs.
/// By making sure this UTXO is almost always coming on the same order as before it was locked,
/// it will more likely be selected again.
/// </summary>
internal class UTXODeterministicComparer : IComparer < UTXO >
{
static UTXODeterministicComparer ( )
{
_Instance = new UTXODeterministicComparer ( RandomUtils . GetUInt256 ( ) ) ;
}
public UTXODeterministicComparer ( uint256 blind )
{
_blind = blind . ToBytes ( ) ;
}
static UTXODeterministicComparer _Instance ;
private byte [ ] _blind ;
public static UTXODeterministicComparer Instance = > _Instance ;
public int Compare ( [ AllowNull ] UTXO x , [ AllowNull ] UTXO y )
{
if ( x = = null )
throw new ArgumentNullException ( nameof ( x ) ) ;
if ( y = = null )
throw new ArgumentNullException ( nameof ( y ) ) ;
Span < byte > tmpx = stackalloc byte [ 32 ] ;
Span < byte > tmpy = stackalloc byte [ 32 ] ;
x . Outpoint . Hash . ToBytes ( tmpx ) ;
y . Outpoint . Hash . ToBytes ( tmpy ) ;
for ( int i = 0 ; i < 32 ; i + + )
{
if ( ( byte ) ( tmpx [ i ] ^ _blind [ i ] ) < ( byte ) ( tmpy [ i ] ^ _blind [ i ] ) )
{
return 1 ;
}
if ( ( byte ) ( tmpx [ i ] ^ _blind [ i ] ) > ( byte ) ( tmpy [ i ] ^ _blind [ i ] ) )
{
return - 1 ;
}
}
return x . Outpoint . N . CompareTo ( y . Outpoint . N ) ;
}
}
2020-01-06 13:57:32 +01:00
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-04-08 08:20:19 +02:00
2020-04-08 22:14:16 +09:00
var sendersInputType = psbt . GetInputsScriptPubKeyType ( ) ;
if ( sendersInputType is null )
2020-04-08 08:20:19 +02:00
return BadRequest ( CreatePayjoinError ( 400 , "unsupported-inputs" , "Payjoin only support segwit inputs (of the same type)" ) ) ;
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 ;
2020-04-08 22:14:16 +09:00
var receiverInputsType = derivationSchemeSettings . AccountDerivation . ScriptPubKeyType ( ) ;
if ( ! PayjoinClient . SupportedFormats . Contains ( receiverInputsType ) )
2020-04-08 08:20:19 +02:00
{
//this should never happen, unless the store owner changed the wallet mid way through an invoice
return StatusCode ( 500 , CreatePayjoinError ( 500 , "unavailable" , $"This service is unavailable for now" ) ) ;
}
2020-04-08 22:14:16 +09:00
if ( sendersInputType ! = receiverInputsType )
{
return StatusCode ( 503 ,
CreatePayjoinError ( 503 , "out-of-utxos" ,
"We do not have any UTXO available for making a payjoin with the sender's inputs type" ) ) ;
}
2020-03-30 00:28:22 +09:00
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 ( ) ;
2020-04-08 13:46:11 +09:00
Array . Sort ( utxos , UTXODeterministicComparer . Instance ) ;
2020-03-30 00:28:22 +09:00
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 ;
2020-04-13 13:34:23 +09:00
// 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_000 UL )
{
if ( RandomUtils . GetUInt32 ( ) % 2 = = 0 )
{
roundMultiple = roundMultiple / 10 ;
}
else
{
randomChangeAmount = ( randomChangeAmount / roundMultiple ) * roundMultiple ;
break ;
}
}
2020-04-07 18:14:31 +09:00
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 ( ) ;
2020-04-25 17:19:24 +02:00
var coins = psbt . Inputs . Select ( i = > i . GetSignableCoin ( ) )
. Concat ( selectedUTXOs . Select ( o = > o . Value . AsCoin ( derivationSchemeSettings . AccountDerivation ) ) ) . ToArray ( ) ;
txBuilder . AddCoins ( coins ) ;
2020-03-30 00:28:22 +09:00
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 ) ;
2020-04-17 11:55:24 +02:00
var coin = selectedUtxo . AsCoin ( derivationSchemeSettings . AccountDerivation ) ;
signedInput . UpdateFromCoin ( coin ) ;
2020-03-30 00:28:22 +09:00
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-26 00:26:02 +09:00
CoinjoinTransactionHash = GetExpectedHash ( newPsbt , coins ) ,
2020-04-05 22:44:34 +09:00
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
2020-04-23 15:02:00 +02:00
if ( psbtFormat & & HexEncoder . IsWellFormed ( rawBody ) )
{
return Ok ( newPsbt . ToHex ( ) ) ;
}
else if ( psbtFormat )
{
2020-03-30 00:28:22 +09:00
return Ok ( newPsbt . ToBase64 ( ) ) ;
2020-04-23 15:02:00 +02:00
}
2020-03-30 00:28:22 +09:00
else
return Ok ( newTx . ToHex ( ) ) ;
}
2020-04-26 00:26:02 +09:00
private uint256 GetExpectedHash ( PSBT psbt , Coin [ ] coins )
2020-04-25 17:19:24 +02:00
{
2020-04-26 00:26:02 +09:00
psbt = psbt . Clone ( ) ;
psbt . AddCoins ( coins ) ;
if ( ! psbt . TryGetFinalizedHash ( out var hash ) )
throw new InvalidOperationException ( "Unable to get the finalized hash" ) ;
return hash ;
2020-04-25 17:19:24 +02:00
}
2020-03-30 00:28:22 +09:00
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
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
}
}
}