2022-10-20 11:19:48 +09:00
#nullable enable
2020-01-06 13:57:32 +01:00
using System ;
using System.Collections.Generic ;
2023-02-07 16:43:31 +09:00
using System.Diagnostics ;
2020-06-28 17:55:27 +09:00
using System.Diagnostics.CodeAnalysis ;
2020-01-06 13:57:32 +01:00
using System.IO ;
using System.Linq ;
using System.Text ;
using System.Threading.Tasks ;
2021-03-01 14:44:53 +01:00
using BTCPayServer.BIP78.Sender ;
2020-08-28 08:49:13 +02:00
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 ;
2021-11-22 17:16:08 +09:00
using BTCPayServer.Logging ;
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 ;
2020-12-12 14:10:47 +09:00
using BTCPayServer.Services.Labels ;
2020-01-06 13:57:32 +01:00
using BTCPayServer.Services.Stores ;
using BTCPayServer.Services.Wallets ;
using Microsoft.AspNetCore.Cors ;
using Microsoft.AspNetCore.Mvc ;
using NBitcoin ;
2020-06-28 17:55:27 +09:00
using NBitcoin.Crypto ;
using NBitcoin.DataEncoders ;
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 ;
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 ( ) ;
}
2020-06-28 22:07:48 -05:00
static readonly UTXODeterministicComparer _Instance ;
private readonly byte [ ] _blind ;
2020-04-08 13:46:11 +09:00
public static UTXODeterministicComparer Instance = > _Instance ;
public int Compare ( [ AllowNull ] UTXO x , [ AllowNull ] UTXO y )
{
2021-12-28 17:39:54 +09:00
ArgumentNullException . ThrowIfNull ( x ) ;
ArgumentNullException . ThrowIfNull ( y ) ;
2020-04-08 13:46:11 +09:00
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 ;
2022-10-11 17:34:29 +09:00
private readonly WalletRepository _walletRepository ;
2020-01-06 13:57:32 +01:00
private readonly ExplorerClientProvider _explorerClientProvider ;
private readonly BTCPayWalletProvider _btcPayWalletProvider ;
2022-07-23 13:26:13 +02:00
private readonly UTXOLocker _utxoLocker ;
2020-03-30 00:28:22 +09:00
private readonly EventAggregator _eventAggregator ;
private readonly NBXplorerDashboard _dashboard ;
private readonly DelayedTransactionBroadcaster _broadcaster ;
2020-05-19 20:55:42 +09:00
private readonly BTCPayServerEnvironment _env ;
2021-04-13 05:26:36 +02:00
private readonly WalletReceiveService _walletReceiveService ;
private readonly StoreRepository _storeRepository ;
2021-10-05 11:10:41 +02:00
private readonly PaymentService _paymentService ;
2020-01-06 13:57:32 +01:00
2021-11-22 17:16:08 +09:00
public Logs Logs { get ; }
2020-01-06 13:57:32 +01:00
public PayJoinEndpointController ( BTCPayNetworkProvider btcPayNetworkProvider ,
InvoiceRepository invoiceRepository , ExplorerClientProvider explorerClientProvider ,
2022-10-11 17:34:29 +09:00
WalletRepository walletRepository ,
2021-03-24 13:48:33 +09:00
BTCPayWalletProvider btcPayWalletProvider ,
2022-07-23 13:26:13 +02:00
UTXOLocker utxoLocker ,
2020-03-30 00:28:22 +09:00
EventAggregator eventAggregator ,
NBXplorerDashboard dashboard ,
2020-04-28 08:06:28 +02:00
DelayedTransactionBroadcaster broadcaster ,
2021-04-13 05:26:36 +02:00
BTCPayServerEnvironment env ,
WalletReceiveService walletReceiveService ,
2021-10-05 11:10:41 +02:00
StoreRepository storeRepository ,
2021-11-22 17:16:08 +09:00
PaymentService paymentService ,
Logs logs )
2020-01-06 13:57:32 +01:00
{
_btcPayNetworkProvider = btcPayNetworkProvider ;
_invoiceRepository = invoiceRepository ;
2022-10-11 17:34:29 +09:00
_walletRepository = walletRepository ;
2020-01-06 13:57:32 +01:00
_explorerClientProvider = explorerClientProvider ;
_btcPayWalletProvider = btcPayWalletProvider ;
2022-07-23 13:26:13 +02:00
_utxoLocker = utxoLocker ;
2020-03-30 00:28:22 +09:00
_eventAggregator = eventAggregator ;
_dashboard = dashboard ;
_broadcaster = broadcaster ;
2020-05-19 20:55:42 +09:00
_env = env ;
2021-04-13 05:26:36 +02:00
_walletReceiveService = walletReceiveService ;
_storeRepository = storeRepository ;
2021-10-05 11:10:41 +02:00
_paymentService = paymentService ;
2021-11-22 17:16:08 +09:00
Logs = logs ;
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-05-09 19:59:21 +09:00
public async Task < IActionResult > Submit ( string cryptoCode ,
2020-06-17 21:43:56 +09:00
long? maxadditionalfeecontribution ,
int? additionalfeeoutputindex ,
2020-05-17 22:21:35 +09:00
decimal minfeerate = - 1.0 m ,
2020-06-17 21:43:56 +09:00
bool disableoutputsubstitution = false ,
2020-05-09 19:59:21 +09:00
int v = 1 )
2020-01-06 13:57:32 +01:00
{
2020-05-28 13:35:48 +09:00
var network = _btcPayNetworkProvider . GetNetwork < BTCPayNetwork > ( cryptoCode ) ;
if ( network = = null )
return NotFound ( ) ;
2020-05-09 19:59:21 +09:00
if ( v ! = 1 )
{
return BadRequest ( new JObject
{
new JProperty ( "errorCode" , "version-unsupported" ) ,
new JProperty ( "supported" , new JArray ( 1 ) ) ,
new JProperty ( "message" , "This version of payjoin is not supported." )
} ) ;
}
2020-05-19 20:55:42 +09:00
2022-07-23 13:26:13 +02:00
await using var ctx = new PayjoinReceiverContext ( _invoiceRepository , _explorerClientProvider . GetExplorerClient ( network ) , _utxoLocker , Logs ) ;
2020-05-19 20:55:42 +09:00
ObjectResult CreatePayjoinErrorAndLog ( int httpCode , PayjoinReceiverWellknownErrors err , string debug )
{
2020-08-28 08:49:13 +02:00
ctx . Logs . Write ( $"Payjoin error: {debug}" , InvoiceEventData . EventSeverity . Error ) ;
2020-05-19 20:55:42 +09:00
return StatusCode ( httpCode , CreatePayjoinError ( err , debug ) ) ;
}
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 ,
2020-05-09 19:29:05 +09:00
CreatePayjoinError ( "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 ,
2020-05-09 19:29:05 +09:00
CreatePayjoinError ( "missing-content-length" ,
2020-03-30 00:28:22 +09:00
"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
}
2022-10-20 11:19:48 +09:00
FeeRate ? originalFeeRate = null ;
2020-03-30 00:28:22 +09:00
bool psbtFormat = true ;
2020-05-09 19:59:21 +09:00
if ( PSBT . TryParse ( rawBody , network . NBitcoinNetwork , out var psbt ) )
{
if ( ! psbt . IsAllFinalized ( ) )
2020-05-28 13:35:48 +09:00
return BadRequest ( CreatePayjoinError ( "original-psbt-rejected" , "The PSBT should be finalized" ) ) ;
2020-05-19 20:55:42 +09:00
ctx . OriginalTransaction = psbt . ExtractTransaction ( ) ;
2020-05-09 19:59:21 +09:00
}
// BTCPay Server implementation support a transaction instead of PSBT
else
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 ) )
2020-05-28 13:35:48 +09:00
return BadRequest ( CreatePayjoinError ( "original-psbt-rejected" , "invalid transaction or psbt" ) ) ;
2020-05-19 20:55:42 +09:00
ctx . OriginalTransaction = tx ;
2020-03-30 00:28:22 +09:00
psbt = PSBT . FromTransaction ( tx , network . NBitcoinNetwork ) ;
2020-05-09 19:59:21 +09:00
psbt = ( await explorer . UpdatePSBTAsync ( new UpdatePSBTRequest ( ) { PSBT = psbt } ) ) . PSBT ;
2020-03-30 00:28:22 +09:00
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-04-05 22:44:34 +09:00
2022-10-20 11:19:48 +09:00
FeeRate ? senderMinFeeRate = minfeerate > = 0.0 m ? new FeeRate ( minfeerate ) : null ;
2020-06-17 21:43:56 +09:00
Money allowedSenderFeeContribution = Money . Satoshis ( maxadditionalfeecontribution is long t & & t > = 0 ? t : 0 ) ;
2020-04-08 08:20:19 +02:00
2020-04-08 22:14:16 +09:00
var sendersInputType = psbt . GetInputsScriptPubKeyType ( ) ;
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-05-28 13:35:48 +09:00
return BadRequest ( CreatePayjoinError ( "original-psbt-rejected" , $"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-05-28 13:35:48 +09:00
return BadRequest ( CreatePayjoinError ( "original-psbt-rejected" ,
2020-03-30 00:28:22 +09:00
"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-05-28 13:35:48 +09:00
return BadRequest ( CreatePayjoinError ( "original-psbt-rejected" ,
2020-03-30 00:28:22 +09:00
"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-05-28 13:35:48 +09:00
return BadRequest ( CreatePayjoinError ( "original-psbt-rejected" ,
2020-03-30 00:28:22 +09:00
"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-05-28 13:35:48 +09:00
return BadRequest ( CreatePayjoinError ( "original-psbt-rejected" , "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-05-19 20:55:42 +09:00
var mempool = await explorer . BroadcastAsync ( ctx . OriginalTransaction , true ) ;
2020-03-30 00:28:22 +09:00
if ( ! mempool . Success )
2020-01-06 13:57:32 +01:00
{
2020-05-19 20:55:42 +09:00
ctx . DoNotBroadcast ( ) ;
2020-05-28 13:35:48 +09:00
return BadRequest ( CreatePayjoinError ( "original-psbt-rejected" ,
2020-03-30 00:28:22 +09:00
$"Provided transaction isn't mempool eligible {mempool.RPCCodeMessage}" ) ) ;
2020-01-06 13:57:32 +01:00
}
2020-05-25 06:47:43 +09:00
var enforcedLowR = ctx . OriginalTransaction . Inputs . All ( IsLowR ) ;
2020-03-30 00:28:22 +09:00
var paymentMethodId = new PaymentMethodId ( network . CryptoCode , PaymentTypes . BTCLike ) ;
2022-10-20 11:19:48 +09:00
Money ? due = null ;
2020-03-30 00:28:22 +09:00
Dictionary < OutPoint , UTXO > selectedUTXOs = new Dictionary < OutPoint , UTXO > ( ) ;
2022-10-20 11:19:48 +09:00
PSBTOutput ? originalPaymentOutput = null ;
BitcoinAddress ? paymentAddress = null ;
KeyPath ? paymentAddressIndex = null ;
InvoiceEntity ? invoice = null ;
DerivationSchemeSettings ? derivationSchemeSettings = null ;
WalletId ? walletId = null ;
2020-03-30 00:28:22 +09:00
foreach ( var output in psbt . Outputs )
2020-01-06 13:57:32 +01:00
{
2021-04-13 05:26:36 +02:00
var walletReceiveMatch =
_walletReceiveService . GetByScriptPubKey ( network . CryptoCode , output . ScriptPubKey ) ;
if ( walletReceiveMatch is null )
{
var key = output . ScriptPubKey . Hash + "#" + network . CryptoCode . ToUpperInvariant ( ) ;
2021-12-31 16:59:02 +09:00
invoice = ( await _invoiceRepository . GetInvoicesFromAddresses ( new [ ] { key } ) ) . FirstOrDefault ( ) ;
2021-04-13 05:26:36 +02:00
if ( invoice is null )
continue ;
derivationSchemeSettings = invoice
. GetSupportedPaymentMethod < DerivationSchemeSettings > ( paymentMethodId )
. SingleOrDefault ( ) ;
walletId = new WalletId ( invoice . StoreId , network . CryptoCode . ToUpperInvariant ( ) ) ;
2022-10-20 11:19:48 +09:00
ctx . Invoice = invoice ;
2021-04-13 05:26:36 +02:00
}
else
{
var store = await _storeRepository . FindStore ( walletReceiveMatch . Item1 . StoreId ) ;
2022-10-20 11:19:48 +09:00
if ( store ! = null )
{
derivationSchemeSettings = store . GetDerivationSchemeSettings ( _btcPayNetworkProvider ,
walletReceiveMatch . Item1 . CryptoCode ) ;
walletId = walletReceiveMatch . Item1 ;
}
2021-04-13 05:26:36 +02:00
}
2021-12-31 16:59:02 +09:00
2020-03-30 00:28:22 +09:00
if ( derivationSchemeSettings is null )
continue ;
2020-04-08 22:14:16 +09:00
var receiverInputsType = derivationSchemeSettings . AccountDerivation . ScriptPubKeyType ( ) ;
2021-03-01 14:44:53 +01:00
if ( receiverInputsType = = ScriptPubKeyType . Legacy )
2020-04-08 08:20:19 +02:00
{
//this should never happen, unless the store owner changed the wallet mid way through an invoice
2020-05-19 20:55:42 +09:00
return CreatePayjoinErrorAndLog ( 503 , PayjoinReceiverWellknownErrors . Unavailable , "Our wallet does not support payjoin" ) ;
2020-04-08 08:20:19 +02:00
}
2020-06-17 21:43:56 +09:00
if ( sendersInputType is ScriptPubKeyType t1 & & t1 ! = receiverInputsType )
2020-04-08 22:14:16 +09:00
{
2020-05-19 20:55:42 +09:00
return CreatePayjoinErrorAndLog ( 503 , PayjoinReceiverWellknownErrors . Unavailable , "We do not have any UTXO available for making a payjoin with the sender's inputs type" ) ;
2020-04-08 22:14:16 +09:00
}
2021-04-13 05:26:36 +02:00
2022-10-20 11:19:48 +09:00
if ( walletReceiveMatch is null & & invoice is not null )
2020-03-30 00:28:22 +09:00
{
2021-04-13 05:26:36 +02:00
var paymentMethod = invoice . GetPaymentMethod ( paymentMethodId ) ;
2023-04-24 14:50:31 +02:00
var paymentDetails = paymentMethod ? . GetPaymentMethodDetails ( ) as Payments . Bitcoin . BitcoinLikeOnChainPaymentMethod ;
if ( paymentMethod is null | | paymentDetails is null | | ! paymentDetails . PayjoinEnabled )
2021-04-13 05:26:36 +02:00
continue ;
2023-07-19 18:47:32 +09:00
due = Money . Coins ( paymentMethod . Calculate ( ) . TotalDue ) - output . Value ;
2021-04-13 05:26:36 +02:00
if ( due > Money . Zero )
{
break ;
}
2020-01-06 13:57:32 +01:00
2021-04-13 05:26:36 +02:00
paymentAddress = paymentDetails . GetDepositAddress ( network . NBitcoinNetwork ) ;
paymentAddressIndex = paymentDetails . KeyPath ;
2021-05-14 16:16:19 +09:00
if ( invoice . GetAllBitcoinPaymentData ( false ) . Any ( ) )
2021-04-13 05:26:36 +02:00
{
ctx . DoNotBroadcast ( ) ;
return UnprocessableEntity ( CreatePayjoinError ( "already-paid" ,
$"The invoice this PSBT is paying has already been partially or completely paid" ) ) ;
}
}
2022-10-20 11:19:48 +09:00
else if ( walletReceiveMatch is not null )
2020-03-30 00:28:22 +09:00
{
2021-04-13 05:26:36 +02:00
due = Money . Zero ;
paymentAddress = walletReceiveMatch . Item2 . Address ;
paymentAddressIndex = walletReceiveMatch . Item2 . KeyPath ;
2020-03-30 00:28:22 +09:00
}
2020-01-06 13:57:32 +01:00
2021-04-13 05:26:36 +02:00
2022-07-23 13:26:13 +02:00
if ( ! await _utxoLocker . TryLockInputs ( ctx . OriginalTransaction . Inputs . Select ( i = > i . PrevOut ) . ToArray ( ) ) )
2020-03-30 00:28:22 +09:00
{
2021-03-24 13:48:33 +09:00
// We do not broadcast, since we might double spend a delayed transaction of a previous payjoin
ctx . DoNotBroadcast ( ) ;
2020-05-19 20:55:42 +09:00
return CreatePayjoinErrorAndLog ( 503 , PayjoinReceiverWellknownErrors . Unavailable , "Some of those inputs have already been used to make another payjoin transaction" ) ;
2020-03-30 00:28:22 +09:00
}
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.
2020-05-19 20:55:42 +09:00
var prevOuts = ctx . OriginalTransaction . Inputs . Select ( o = > o . PrevOut ) . ToHashSet ( ) ;
2020-03-30 00:28:22 +09:00
utxos = utxos . Where ( u = > ! prevOuts . Contains ( u . Outpoint ) ) . ToArray ( ) ;
2020-04-08 13:46:11 +09:00
Array . Sort ( utxos , UTXODeterministicComparer . Instance ) ;
2022-10-20 11:19:48 +09:00
foreach ( var utxo in ( await SelectUTXO ( network ,
utxos ,
psbt . Inputs . Select ( input = > input . GetCoin ( ) ? . Amount . ToDecimal ( MoneyUnit . BTC ) )
. Where ( o = > o . HasValue )
. Select ( o = > o ! . Value ) . ToArray ( ) ,
output . Value . ToDecimal ( MoneyUnit . BTC ) ,
psbt . Outputs . Where ( psbtOutput = > psbtOutput . Index ! = output . Index ) . Select ( psbtOutput = > psbtOutput . Value . ToDecimal ( MoneyUnit . BTC ) ) ) ) . selectedUTXO )
2020-03-30 00:28:22 +09:00
{
selectedUTXOs . Add ( utxo . Outpoint , utxo ) ;
}
2020-05-19 20:55:42 +09:00
ctx . LockedUTXOs = selectedUTXOs . Select ( u = > u . Key ) . ToArray ( ) ;
2020-04-07 18:14:31 +09:00
originalPaymentOutput = output ;
2020-03-30 00:28:22 +09:00
break ;
2020-01-06 13:57:32 +01:00
}
2022-10-20 11:19:48 +09:00
if ( paymentAddress is null | |
paymentAddressIndex is null | |
walletId is null | |
derivationSchemeSettings is null | |
originalPaymentOutput is null )
2020-01-06 13:57:32 +01:00
{
2022-10-20 11:19:48 +09:00
if ( due is not null & & due > Money . Zero )
return InvoiceNotFullyPaid ( ) ;
2020-05-09 19:29:05 +09:00
return BadRequest ( CreatePayjoinError ( "invoice-not-found" ,
2020-03-30 00:28:22 +09:00
"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 )
2022-10-20 11:19:48 +09:00
return InvoiceNotFullyPaid ( ) ;
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-05-19 20:55:42 +09:00
return CreatePayjoinErrorAndLog ( 503 , PayjoinReceiverWellknownErrors . Unavailable , "We do not have any UTXO available for contributing to a payjoin" ) ;
2020-01-06 13:57:32 +01:00
}
2020-05-19 20:55:42 +09:00
await _broadcaster . Schedule ( DateTimeOffset . UtcNow + TimeSpan . FromMinutes ( 2.0 ) , ctx . OriginalTransaction , 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
{
2023-04-26 05:29:06 +09:00
// This should not happen, as we check the existence of private key before creating invoice with payjoin
2020-05-19 20:55:42 +09:00
return CreatePayjoinErrorAndLog ( 503 , PayjoinReceiverWellknownErrors . Unavailable , "The HD Key of the store changed" ) ;
2020-01-06 13:57:32 +01:00
}
2020-05-19 20:55:42 +09:00
2020-04-07 18:14:31 +09:00
Money contributedAmount = Money . Zero ;
2020-05-19 20:55:42 +09:00
var newTx = ctx . OriginalTransaction . Clone ( ) ;
2020-04-07 18:14:31 +09:00
var ourNewOutput = newTx . Outputs [ originalPaymentOutput . Index ] ;
HashSet < TxOut > isOurOutput = new HashSet < TxOut > ( ) ;
isOurOutput . Add ( ourNewOutput ) ;
2022-10-20 11:19:48 +09:00
TxOut ? feeOutput =
2020-06-17 21:43:56 +09:00
additionalfeeoutputindex is int feeOutputIndex & &
maxadditionalfeecontribution is long v3 & &
v3 > = 0 & &
feeOutputIndex > = 0
& & feeOutputIndex < newTx . Outputs . Count
& & ! isOurOutput . Contains ( newTx . Outputs [ feeOutputIndex ] )
? newTx . Outputs [ feeOutputIndex ] : null ;
2020-05-05 04:44:55 +09:00
int senderInputCount = newTx . Inputs . Count ;
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-05-05 04:44:55 +09:00
var newInput = newTx . Inputs . Add ( selectedUTXO . Outpoint ) ;
2021-03-23 17:53:23 +09:00
newInput . Sequence = newTx . Inputs [ ( int ) ( RandomUtils . GetUInt32 ( ) % senderInputCount ) ] . Sequence ;
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 ) ;
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 ;
2020-05-17 05:07:24 +09:00
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)
2022-10-20 11:19:48 +09:00
for ( int i = 0 ; i < newTx . Outputs . Count & & additionalFee > Money . Zero & & due < Money . Zero & & ! ( invoice ? . IsUnsetTopUp ( ) is true ) ; i + + )
2020-01-06 13:57:32 +01:00
{
2020-06-17 21:43:56 +09:00
if ( disableoutputsubstitution )
break ;
2020-04-07 18:14:31 +09:00
if ( isOurOutput . Contains ( newTx . Outputs [ i ] ) )
{
var outputContribution = Money . Min ( additionalFee , - due ) ;
outputContribution = Money . Min ( outputContribution ,
2021-11-15 06:48:07 +02:00
newTx . Outputs [ i ] . Value - newTx . Outputs [ i ] . GetDustThreshold ( ) ) ;
2020-04-07 18:14:31 +09:00
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-06-17 21:43:56 +09:00
if ( feeOutput ! = null )
2020-03-05 19:04:08 +01:00
{
2020-06-17 21:43:56 +09:00
var outputContribution = Money . Min ( additionalFee , feeOutput . Value ) ;
outputContribution = Money . Min ( outputContribution ,
2021-11-15 06:48:07 +02:00
feeOutput . Value - feeOutput . GetDustThreshold ( ) ) ;
2020-06-17 21:43:56 +09:00
outputContribution = Money . Min ( outputContribution , allowedSenderFeeContribution ) ;
feeOutput . Value - = outputContribution ;
additionalFee - = outputContribution ;
allowedSenderFeeContribution - = outputContribution ;
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 ) ) ;
2020-05-17 22:21:35 +09:00
if ( new FeeRate ( newFeePaid , newVSize ) < ( senderMinFeeRate ? ? minRelayTxFee ) )
2020-03-30 00:28:22 +09:00
{
2020-05-19 20:55:42 +09:00
return CreatePayjoinErrorAndLog ( 422 , PayjoinReceiverWellknownErrors . NotEnoughMoney , "Not enough money is sent to pay for the additional payjoin inputs" ) ;
2020-03-30 00:28:22 +09:00
}
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 ) ;
2022-10-20 11:19:48 +09:00
if ( signedInput is not null )
2020-05-25 06:47:43 +09:00
{
2022-10-20 11:19:48 +09:00
signedInput . UpdateFromCoin ( coin ) ;
var privateKey = accountKey . Derive ( selectedUtxo . KeyPath ) . PrivateKey ;
signedInput . PSBT . Settings . SigningOptions = new SigningOptions ( )
{
EnforceLowR = enforcedLowR
} ;
signedInput . Sign ( privateKey ) ;
signedInput . FinalizeInput ( ) ;
newTx . Inputs [ signedInput . Index ] . WitScript = newPsbt . Inputs [ ( int ) signedInput . Index ] . FinalScriptWitness ;
}
2020-03-30 00:28:22 +09:00
}
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 ,
2020-05-19 20:55:42 +09:00
new OutPoint ( ctx . OriginalTransaction . GetHash ( ) , originalPaymentOutput . Index ) ,
2020-08-09 16:00:58 +02:00
ctx . OriginalTransaction . RBF , paymentAddressIndex ) ;
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 ) ,
2022-10-20 11:19:48 +09:00
CoinjoinValue = originalPaymentOutput . Value - ourFeeContribution ,
2020-03-30 00:28:22 +09:00
ContributedOutPoints = selectedUTXOs . Select ( o = > o . Key ) . ToArray ( )
2020-01-06 13:57:32 +01:00
} ;
2022-10-20 11:19:48 +09:00
if ( invoice is not null )
2020-04-05 22:44:34 +09:00
{
2021-10-05 11:10:41 +02:00
var payment = await _paymentService . AddPayment ( invoice . Id , DateTimeOffset . UtcNow , originalPaymentData , network , true ) ;
2021-04-13 05:26:36 +02:00
if ( payment is null )
{
return UnprocessableEntity ( CreatePayjoinError ( "already-paid" ,
$"The original transaction has already been accounted" ) ) ;
}
2021-12-31 16:59:02 +09:00
_eventAggregator . Publish ( new InvoiceEvent ( invoice , InvoiceEvent . ReceivedPayment ) { Payment = payment } ) ;
2020-04-05 22:44:34 +09:00
}
2021-04-13 05:26:36 +02:00
2020-05-19 20:55:42 +09:00
await _btcPayWalletProvider . GetWallet ( network ) . SaveOffchainTransactionAsync ( ctx . OriginalTransaction ) ;
2022-10-11 17:34:29 +09:00
foreach ( var utxo in selectedUTXOs )
2020-04-28 08:06:28 +02:00
{
2022-10-11 17:34:29 +09:00
await _walletRepository . AddWalletTransactionAttachment ( walletId , utxo . Key . Hash , Attachment . PayjoinExposed ( invoice ? . Id ) ) ;
}
await _walletRepository . AddWalletTransactionAttachment ( walletId , originalPaymentData . PayjoinInformation . CoinjoinTransactionHash , Attachment . Payjoin ( ) ) ;
2023-01-06 14:18:07 +01:00
2020-05-19 20:55:42 +09:00
ctx . Success ( ) ;
2020-05-09 19:59:21 +09:00
// BTCPay Server support PSBT set as hex
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-05-09 19:59:21 +09:00
// BTCPay Server should returns transaction if received transaction
2020-03-30 00:28:22 +09:00
else
return Ok ( newTx . ToHex ( ) ) ;
}
2022-10-20 11:19:48 +09:00
private IActionResult InvoiceNotFullyPaid ( )
{
return BadRequest ( CreatePayjoinError ( "invoice-not-fully-paid" ,
"The transaction must pay the whole invoice" ) ) ;
}
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-05-09 19:29:05 +09:00
private JObject CreatePayjoinError ( string errorCode , string friendlyMessage )
2020-03-30 00:28:22 +09:00
{
var o = new JObject ( ) ;
o . Add ( new JProperty ( "errorCode" , errorCode ) ) ;
o . Add ( new JProperty ( "message" , friendlyMessage ) ) ;
return o ;
2020-01-06 13:57:32 +01:00
}
2020-05-19 20:55:42 +09:00
private JObject CreatePayjoinError ( PayjoinReceiverWellknownErrors error , string debug )
{
var o = new JObject ( ) ;
o . Add ( new JProperty ( "errorCode" , PayjoinReceiverHelper . GetErrorCode ( error ) ) ) ;
2020-07-30 20:43:44 -05:00
if ( string . IsNullOrEmpty ( debug ) | | ! _env . IsDeveloping )
2020-05-19 20:55:42 +09:00
{
o . Add ( new JProperty ( "message" , PayjoinReceiverHelper . GetMessage ( error ) ) ) ;
}
else
{
o . Add ( new JProperty ( "message" , debug ) ) ;
}
return o ;
}
2020-04-27 18:28:21 +02:00
public enum PayjoinUtxoSelectionType
{
Unavailable ,
HeuristicBased ,
Ordered
}
[NonAction]
2023-02-07 16:43:31 +09:00
public async Task < ( UTXO [ ] selectedUTXO , PayjoinUtxoSelectionType selectionType ) > SelectUTXO ( BTCPayNetwork network , UTXO [ ] availableUtxos , decimal [ ] otherInputs , decimal originalPaymentOutput ,
2020-04-27 18:28:21 +02:00
IEnumerable < decimal > otherOutputs )
2020-01-06 13:57:32 +01:00
{
2020-03-30 00:28:22 +09:00
if ( availableUtxos . Length = = 0 )
2020-04-27 18:28:21 +02:00
return ( Array . Empty < UTXO > ( ) , PayjoinUtxoSelectionType . Unavailable ) ;
2020-05-19 20:55:42 +09:00
HashSet < OutPoint > locked = new HashSet < OutPoint > ( ) ;
2023-02-07 16:43:31 +09:00
TimeSpan timeout = TimeSpan . FromSeconds ( 30.0 ) ;
Stopwatch watch = new Stopwatch ( ) ;
watch . Start ( ) ;
// BlockSci UIH1 and UIH2:
// if min(out) < min(in) then UIH1 else UIH2
// https://eprint.iacr.org/2022/589.pdf
var origMinOut = otherOutputs . Concat ( new [ ] { decimal . MaxValue } ) . Min ( ) ;
var origMinIn = otherInputs . Concat ( new [ ] { decimal . MaxValue } ) . Min ( ) ;
2020-05-19 20:55:42 +09:00
2020-01-06 13:57:32 +01:00
foreach ( var availableUtxo in availableUtxos )
{
2023-02-07 16:43:31 +09:00
if ( watch . Elapsed > timeout )
2020-03-30 00:28:22 +09:00
break ;
2020-04-27 18:28:21 +02:00
2023-02-07 16:43:31 +09:00
var utxoValue = availableUtxo . Value . GetValue ( network ) ;
var newPaymentOutput = originalPaymentOutput + utxoValue ;
var minOut = Math . Min ( origMinOut , newPaymentOutput ) ;
var minIn = Math . Min ( origMinIn , utxoValue ) ;
if ( minOut < minIn )
2020-01-06 13:57:32 +01:00
{
2023-02-07 16:43:31 +09:00
// UIH1: Optimal change, smallest output is the change address.
if ( await _utxoLocker . TryLock ( availableUtxo . Outpoint ) )
2020-04-27 18:28:21 +02:00
{
2023-02-07 16:43:31 +09:00
return ( new [ ] { availableUtxo } , PayjoinUtxoSelectionType . HeuristicBased ) ;
}
else
{
locked . Add ( availableUtxo . Outpoint ) ;
continue ;
2020-04-27 18:28:21 +02:00
}
2020-01-06 13:57:32 +01:00
}
2023-02-07 16:43:31 +09:00
else
2020-04-27 18:28:21 +02:00
{
2023-02-07 16:43:31 +09:00
// UIH2: Unnecessary input, let's try to get an optimal change by using a different output
2020-04-27 18:28:21 +02:00
continue ;
}
2020-01-06 13:57:32 +01:00
}
2023-02-07 16:43:31 +09:00
2020-03-30 00:28:22 +09:00
foreach ( var utxo in availableUtxos . Where ( u = > ! locked . Contains ( u . Outpoint ) ) )
{
2023-02-07 16:43:31 +09:00
if ( watch . Elapsed > timeout )
2020-03-30 00:28:22 +09:00
break ;
2022-07-23 13:26:13 +02:00
if ( await _utxoLocker . TryLock ( utxo . Outpoint ) )
2020-03-30 00:28:22 +09:00
{
2020-05-19 20:55:42 +09:00
return ( new [ ] { utxo } , PayjoinUtxoSelectionType . Ordered ) ;
2020-03-30 00:28:22 +09:00
}
2023-02-07 16:43:31 +09:00
locked . Add ( utxo . Outpoint ) ;
2020-03-30 00:28:22 +09:00
}
2020-04-27 18:28:21 +02:00
return ( Array . Empty < UTXO > ( ) , PayjoinUtxoSelectionType . Unavailable ) ;
2020-01-06 13:57:32 +01:00
}
2020-05-25 06:47:43 +09:00
private static bool IsLowR ( TxIn txin )
{
IEnumerable < byte [ ] > pushes = txin . WitScript . PushCount > 0 ? txin . WitScript . Pushes :
txin . ScriptSig . IsPushOnly ? txin . ScriptSig . ToOps ( ) . Select ( o = > o . PushData ) :
Array . Empty < byte [ ] > ( ) ;
return pushes . Where ( p = > ECDSASignature . IsValidDER ( p ) ) . All ( p = > p . Length < = 71 ) ;
}
2020-01-06 13:57:32 +01:00
}
}