2020-03-29 17:28:22 +02:00
using System ;
using System.Collections.Generic ;
2020-05-09 12:29:05 +02:00
using System.Configuration ;
2020-05-17 15:21:35 +02:00
using System.Globalization ;
2020-03-29 17:28:22 +02:00
using System.Linq ;
using System.Net.Http ;
using System.Text ;
using System.Threading ;
using System.Threading.Tasks ;
2020-05-19 13:55:42 +02:00
using BTCPayServer.Payments.Changelly.Models ;
2020-04-09 10:38:55 +02:00
using Google.Apis.Http ;
2020-03-29 17:28:22 +02:00
using NBitcoin ;
using Newtonsoft.Json ;
using Newtonsoft.Json.Linq ;
2020-04-09 10:38:55 +02:00
using IHttpClientFactory = System . Net . Http . IHttpClientFactory ;
2020-03-29 17:28:22 +02:00
namespace BTCPayServer.Services
{
2020-04-08 08:20:19 +02:00
public static class PSBTExtensions
{
2020-04-08 15:14:16 +02:00
public static ScriptPubKeyType ? GetInputsScriptPubKeyType ( this PSBT psbt )
2020-04-08 08:20:19 +02:00
{
2020-04-08 15:14:16 +02:00
if ( ! psbt . IsAllFinalized ( ) | | psbt . Inputs . Any ( i = > i . WitnessUtxo = = null ) )
throw new InvalidOperationException ( "The psbt should be finalized with witness information" ) ;
var coinsPerTypes = psbt . Inputs . Select ( i = >
{
2020-04-09 12:44:16 +02:00
return ( ( PSBTCoin ) i , i . GetInputScriptPubKeyType ( ) ) ;
2020-04-08 15:14:16 +02:00
} ) . GroupBy ( o = > o . Item2 , o = > o . Item1 ) . ToArray ( ) ;
if ( coinsPerTypes . Length ! = 1 )
return default ;
return coinsPerTypes [ 0 ] . Key ;
2020-04-08 08:20:19 +02:00
}
2020-04-09 12:44:16 +02:00
public static ScriptPubKeyType ? GetInputScriptPubKeyType ( this PSBTInput i )
{
if ( i . WitnessUtxo . ScriptPubKey . IsScriptType ( ScriptType . P2WPKH ) )
return ScriptPubKeyType . Segwit ;
if ( i . WitnessUtxo . ScriptPubKey . IsScriptType ( ScriptType . P2SH ) & &
2020-05-09 12:29:05 +02:00
PayToWitPubKeyHashTemplate . Instance . ExtractWitScriptParameters ( i . FinalScriptWitness ) is { } )
2020-04-09 12:44:16 +02:00
return ScriptPubKeyType . SegwitP2SH ;
2020-04-17 11:55:24 +02:00
return null ;
2020-04-09 12:44:16 +02:00
}
2020-04-08 08:20:19 +02:00
}
2020-05-16 22:07:24 +02:00
public class PayjoinClientParameters
{
2020-05-17 15:21:35 +02:00
public Money MaxAdditionalFeeContribution { get ; set ; }
public FeeRate MinFeeRate { get ; set ; }
public int? AdditionalFeeOutputIndex { get ; set ; }
2020-05-16 22:07:24 +02:00
public int Version { get ; set ; } = 1 ;
}
2020-03-29 17:28:22 +02:00
public class PayjoinClient
{
2020-04-09 10:38:55 +02:00
public const string PayjoinOnionNamedClient = "payjoin.onion" ;
public const string PayjoinClearnetNamedClient = "payjoin.clearnet" ;
2020-04-08 08:20:19 +02:00
public static readonly ScriptPubKeyType [ ] SupportedFormats = {
ScriptPubKeyType . Segwit ,
ScriptPubKeyType . SegwitP2SH
} ;
2020-04-13 11:52:22 +02:00
public const string BIP21EndpointKey = "pj" ;
2020-04-08 08:20:19 +02:00
2020-03-29 17:28:22 +02:00
private readonly ExplorerClientProvider _explorerClientProvider ;
2020-04-09 10:38:55 +02:00
private IHttpClientFactory _httpClientFactory ;
2020-03-29 17:28:22 +02:00
2020-04-09 10:38:55 +02:00
public PayjoinClient ( ExplorerClientProvider explorerClientProvider , IHttpClientFactory httpClientFactory )
2020-03-29 17:28:22 +02:00
{
2020-05-09 12:29:05 +02:00
if ( httpClientFactory = = null )
throw new ArgumentNullException ( nameof ( httpClientFactory ) ) ;
2020-03-29 17:28:22 +02:00
_explorerClientProvider =
explorerClientProvider ? ? throw new ArgumentNullException ( nameof ( explorerClientProvider ) ) ;
2020-05-09 12:29:05 +02:00
_httpClientFactory = httpClientFactory ;
2020-03-29 17:28:22 +02:00
}
2020-05-16 22:07:24 +02:00
public Money MaxFeeBumpContribution { get ; set ; }
2020-05-17 15:21:35 +02:00
public FeeRate MinimumFeeRate { get ; set ; }
2020-05-16 22:07:24 +02:00
2020-03-29 17:28:22 +02:00
public async Task < PSBT > RequestPayjoin ( Uri endpoint , DerivationSchemeSettings derivationSchemeSettings ,
PSBT originalTx , CancellationToken cancellationToken )
{
2020-05-09 12:29:05 +02:00
if ( endpoint = = null )
throw new ArgumentNullException ( nameof ( endpoint ) ) ;
if ( derivationSchemeSettings = = null )
throw new ArgumentNullException ( nameof ( derivationSchemeSettings ) ) ;
if ( originalTx = = null )
throw new ArgumentNullException ( nameof ( originalTx ) ) ;
2020-04-08 11:24:04 +02:00
if ( originalTx . IsAllFinalized ( ) )
throw new InvalidOperationException ( "The original PSBT should not be finalized." ) ;
2020-05-16 22:07:24 +02:00
var clientParameters = new PayjoinClientParameters ( ) ;
2020-04-08 08:20:19 +02:00
var type = derivationSchemeSettings . AccountDerivation . ScriptPubKeyType ( ) ;
if ( ! SupportedFormats . Contains ( type ) )
{
throw new PayjoinSenderException ( $"The wallet does not support payjoin" ) ;
}
2020-03-29 17:28:22 +02:00
var signingAccount = derivationSchemeSettings . GetSigningAccountKeySettings ( ) ;
2020-05-16 22:07:24 +02:00
var changeOutput = originalTx . Outputs . CoinsFor ( derivationSchemeSettings . AccountDerivation , signingAccount . AccountKey , signingAccount . GetRootedKeyPath ( ) )
. FirstOrDefault ( ) ;
if ( changeOutput is PSBTOutput o )
2020-05-17 15:21:35 +02:00
clientParameters . AdditionalFeeOutputIndex = ( int ) o . Index ;
2020-03-29 17:28:22 +02:00
var sentBefore = - originalTx . GetBalance ( derivationSchemeSettings . AccountDerivation ,
signingAccount . AccountKey ,
signingAccount . GetRootedKeyPath ( ) ) ;
2020-04-06 16:21:02 +02:00
var oldGlobalTx = originalTx . GetGlobalTransaction ( ) ;
if ( ! originalTx . TryGetEstimatedFeeRate ( out var originalFeeRate ) | | ! originalTx . TryGetVirtualSize ( out var oldVirtualSize ) )
2020-03-29 17:28:22 +02:00
throw new ArgumentException ( "originalTx should have utxo information" , nameof ( originalTx ) ) ;
2020-04-06 16:21:02 +02:00
var originalFee = originalTx . GetFee ( ) ;
2020-05-17 15:21:35 +02:00
clientParameters . MaxAdditionalFeeContribution = MaxFeeBumpContribution is null ? originalFee : MaxFeeBumpContribution ;
if ( MinimumFeeRate is FeeRate v )
clientParameters . MinFeeRate = v ;
2020-03-29 17:28:22 +02:00
var cloned = originalTx . Clone ( ) ;
2020-05-16 22:07:24 +02:00
cloned . Finalize ( ) ;
2020-03-29 17:28:22 +02:00
// We make sure we don't send unnecessary information to the receiver
foreach ( var finalized in cloned . Inputs . Where ( i = > i . IsFinalized ( ) ) )
{
finalized . ClearForFinalize ( ) ;
}
foreach ( var output in cloned . Outputs )
{
output . HDKeyPaths . Clear ( ) ;
}
cloned . GlobalXPubs . Clear ( ) ;
2020-05-16 22:07:24 +02:00
endpoint = ApplyOptionalParameters ( endpoint , clientParameters ) ;
2020-04-09 10:38:55 +02:00
using HttpClient client = CreateHttpClient ( endpoint ) ;
2020-04-08 15:40:41 +02:00
var bpuresponse = await client . PostAsync ( endpoint ,
2020-03-29 17:28:22 +02:00
new StringContent ( cloned . ToHex ( ) , Encoding . UTF8 , "text/plain" ) , cancellationToken ) ;
if ( ! bpuresponse . IsSuccessStatusCode )
{
var errorStr = await bpuresponse . Content . ReadAsStringAsync ( ) ;
try
{
var error = JObject . Parse ( errorStr ) ;
2020-05-09 12:29:05 +02:00
throw new PayjoinReceiverException ( error [ "errorCode" ] . Value < string > ( ) ,
2020-03-29 17:28:22 +02:00
error [ "message" ] . Value < string > ( ) ) ;
}
catch ( JsonReaderException )
{
// will throw
bpuresponse . EnsureSuccessStatusCode ( ) ;
throw ;
}
}
var hex = await bpuresponse . Content . ReadAsStringAsync ( ) ;
var newPSBT = PSBT . Parse ( hex , originalTx . Network ) ;
// Checking that the PSBT of the receiver is clean
if ( newPSBT . GlobalXPubs . Any ( ) )
{
throw new PayjoinSenderException ( "GlobalXPubs should not be included in the receiver's PSBT" ) ;
}
if ( newPSBT . Outputs . Any ( o = > o . HDKeyPaths . Count ! = 0 ) | | newPSBT . Inputs . Any ( o = > o . HDKeyPaths . Count ! = 0 ) )
{
throw new PayjoinSenderException ( "Keypath information should not be included in the receiver's PSBT" ) ;
}
////////////
newPSBT = await _explorerClientProvider . UpdatePSBT ( derivationSchemeSettings , newPSBT ) ;
if ( newPSBT . CheckSanity ( ) is IList < PSBTError > errors2 & & errors2 . Count ! = 0 )
{
throw new PayjoinSenderException ( $"The PSBT of the receiver is insane ({errors2[0]})" ) ;
}
// We make sure we don't sign things what should not be signed
foreach ( var finalized in newPSBT . Inputs . Where ( i = > i . IsFinalized ( ) ) )
{
finalized . ClearForFinalize ( ) ;
}
// Make sure only the only our output have any information
foreach ( var output in newPSBT . Outputs )
{
output . HDKeyPaths . Clear ( ) ;
2020-05-09 12:29:05 +02:00
foreach ( var originalOutput in originalTx . Outputs )
2020-03-29 17:28:22 +02:00
{
if ( output . ScriptPubKey = = originalOutput . ScriptPubKey )
output . UpdateFrom ( originalOutput ) ;
}
}
// Making sure that our inputs are finalized, and that some of our inputs have not been added
2020-04-06 16:21:02 +02:00
var newGlobalTx = newPSBT . GetGlobalTransaction ( ) ;
2020-03-29 17:28:22 +02:00
int ourInputCount = 0 ;
2020-04-06 16:21:02 +02:00
if ( newGlobalTx . Version ! = oldGlobalTx . Version )
throw new PayjoinSenderException ( "The version field of the transaction has been modified" ) ;
if ( newGlobalTx . LockTime ! = oldGlobalTx . LockTime )
throw new PayjoinSenderException ( "The LockTime field of the transaction has been modified" ) ;
2020-03-29 17:28:22 +02:00
foreach ( var input in newPSBT . Inputs . CoinsFor ( derivationSchemeSettings . AccountDerivation ,
signingAccount . AccountKey , signingAccount . GetRootedKeyPath ( ) ) )
{
2020-04-08 10:42:50 +02:00
if ( oldGlobalTx . Inputs . FindIndexedInput ( input . PrevOut ) is IndexedTxIn ourInput )
2020-03-29 17:28:22 +02:00
{
ourInputCount + + ;
if ( input . IsFinalized ( ) )
throw new PayjoinSenderException ( "A PSBT input from us should not be finalized" ) ;
2020-04-08 10:51:22 +02:00
if ( newGlobalTx . Inputs [ input . Index ] . Sequence ! = ourInput . TxIn . Sequence )
2020-04-06 16:21:02 +02:00
throw new PayjoinSenderException ( "The sequence of one of our input has been modified" ) ;
2020-03-29 17:28:22 +02:00
}
else
{
throw new PayjoinSenderException (
"The payjoin receiver added some of our own inputs in the proposal" ) ;
}
}
foreach ( var input in newPSBT . Inputs )
{
2020-04-06 16:21:02 +02:00
if ( originalTx . Inputs . FindIndexedInput ( input . PrevOut ) is null )
{
if ( ! input . IsFinalized ( ) )
throw new PayjoinSenderException ( "The payjoin receiver included a non finalized input" ) ;
2020-04-09 12:44:16 +02:00
// Making sure that the receiver's inputs are finalized and match format
var payjoinInputType = input . GetInputScriptPubKeyType ( ) ;
if ( payjoinInputType is null | | payjoinInputType . Value ! = type )
{
throw new PayjoinSenderException ( "The payjoin receiver included an input that is not the same segwit input type" ) ;
}
2020-04-06 16:21:02 +02:00
}
2020-03-29 17:28:22 +02:00
}
if ( ourInputCount < originalTx . Inputs . Count )
throw new PayjoinSenderException ( "The payjoin receiver removed some of our inputs" ) ;
2020-05-17 15:21:35 +02:00
if ( ! newPSBT . TryGetEstimatedFeeRate ( out var newFeeRate ) | | ! newPSBT . TryGetVirtualSize ( out var newVirtualSize ) )
throw new PayjoinSenderException ( "The payjoin receiver did not included UTXO information to calculate fee correctly" ) ;
if ( clientParameters . MinFeeRate is FeeRate minFeeRate )
{
if ( newFeeRate < minFeeRate )
throw new PayjoinSenderException ( "The payjoin receiver created a payjoin with a too low fee rate" ) ;
}
2020-03-29 17:28:22 +02:00
var sentAfter = - newPSBT . GetBalance ( derivationSchemeSettings . AccountDerivation ,
signingAccount . AccountKey ,
signingAccount . GetRootedKeyPath ( ) ) ;
if ( sentAfter > sentBefore )
{
2020-04-06 16:21:02 +02:00
var overPaying = sentAfter - sentBefore ;
var additionalFee = newPSBT . GetFee ( ) - originalFee ;
if ( overPaying > additionalFee )
throw new PayjoinSenderException ( "The payjoin receiver is sending more money to himself" ) ;
2020-05-17 15:21:35 +02:00
if ( overPaying > clientParameters . MaxAdditionalFeeContribution )
2020-05-16 22:07:24 +02:00
throw new PayjoinSenderException ( "The payjoin receiver is making us pay too much fee" ) ;
2020-04-06 16:21:02 +02:00
2020-03-29 17:28:22 +02:00
// Let's check the difference is only for the fee and that feerate
// did not changed that much
2020-04-06 16:21:02 +02:00
var expectedFee = originalFeeRate . GetFee ( newVirtualSize ) ;
2020-03-29 17:28:22 +02:00
// Signing precisely is hard science, give some breathing room for error.
2020-04-06 16:21:02 +02:00
expectedFee + = originalFeeRate . GetFee ( newPSBT . Inputs . Count * 2 ) ;
2020-04-07 08:10:19 +02:00
if ( overPaying > ( expectedFee - originalFee ) )
2020-04-06 16:21:02 +02:00
throw new PayjoinSenderException ( "The payjoin receiver increased the fee rate we are paying too much" ) ;
2020-03-29 17:28:22 +02:00
}
return newPSBT ;
}
2020-04-09 10:38:55 +02:00
2020-05-16 22:07:24 +02:00
private static Uri ApplyOptionalParameters ( Uri endpoint , PayjoinClientParameters clientParameters )
{
var requestUri = endpoint . AbsoluteUri ;
if ( requestUri . IndexOf ( '?' , StringComparison . OrdinalIgnoreCase ) is int i & & i ! = - 1 )
requestUri = requestUri . Substring ( 0 , i ) ;
List < string > parameters = new List < string > ( 3 ) ;
parameters . Add ( $"v={clientParameters.Version}" ) ;
2020-05-17 15:21:35 +02:00
if ( clientParameters . AdditionalFeeOutputIndex is int additionalFeeOutputIndex )
parameters . Add ( $"additionalfeeoutputindex={additionalFeeOutputIndex.ToString(CultureInfo.InvariantCulture)}" ) ;
if ( clientParameters . MaxAdditionalFeeContribution is Money maxAdditionalFeeContribution )
parameters . Add ( $"maxadditionalfeecontribution={maxAdditionalFeeContribution.Satoshi.ToString(CultureInfo.InvariantCulture)}" ) ;
if ( clientParameters . MinFeeRate is FeeRate minFeeRate )
parameters . Add ( $"minfeerate={minFeeRate.SatoshiPerByte.ToString(CultureInfo.InvariantCulture)}" ) ;
2020-05-16 22:07:24 +02:00
endpoint = new Uri ( $"{requestUri}?{string.Join('&', parameters)}" ) ;
return endpoint ;
}
2020-04-09 10:38:55 +02:00
private HttpClient CreateHttpClient ( Uri uri )
{
if ( uri . IsOnion ( ) )
return _httpClientFactory . CreateClient ( PayjoinOnionNamedClient ) ;
else
return _httpClientFactory . CreateClient ( PayjoinClearnetNamedClient ) ;
}
2020-03-29 17:28:22 +02:00
}
public class PayjoinException : Exception
{
public PayjoinException ( string message ) : base ( message )
{
}
}
2020-05-09 12:29:05 +02:00
public enum PayjoinReceiverWellknownErrors
{
Unavailable ,
NotEnoughMoney ,
2020-05-09 12:59:21 +02:00
VersionUnsupported ,
2020-05-28 06:35:48 +02:00
OriginalPSBTRejected
2020-05-09 12:29:05 +02:00
}
2020-05-19 13:55:42 +02:00
public class PayjoinReceiverHelper
{
static IEnumerable < ( PayjoinReceiverWellknownErrors EnumValue , string ErrorCode , string Message ) > Get ( )
{
yield return ( PayjoinReceiverWellknownErrors . Unavailable , "unavailable" , "The payjoin endpoint is not available for now." ) ;
yield return ( PayjoinReceiverWellknownErrors . NotEnoughMoney , "not-enough-money" , "The receiver added some inputs but could not bump the fee of the payjoin proposal." ) ;
yield return ( PayjoinReceiverWellknownErrors . VersionUnsupported , "version-unsupported" , "This version of payjoin is not supported." ) ;
2020-05-28 06:35:48 +02:00
yield return ( PayjoinReceiverWellknownErrors . OriginalPSBTRejected , "original-psbt-rejected" , "The receiver rejected the original PSBT." ) ;
2020-05-19 13:55:42 +02:00
}
public static string GetErrorCode ( PayjoinReceiverWellknownErrors err )
{
return Get ( ) . Single ( o = > o . EnumValue = = err ) . ErrorCode ;
}
public static PayjoinReceiverWellknownErrors ? GetWellknownError ( string errorCode )
{
var t = Get ( ) . FirstOrDefault ( o = > o . ErrorCode = = errorCode ) ;
if ( t = = default )
return null ;
return t . EnumValue ;
}
static string UnknownError = "Unknown error from the receiver" ;
public static string GetMessage ( string errorCode )
{
return Get ( ) . FirstOrDefault ( o = > o . ErrorCode = = errorCode ) . Message ? ? UnknownError ;
}
public static string GetMessage ( PayjoinReceiverWellknownErrors err )
{
return Get ( ) . Single ( o = > o . EnumValue = = err ) . Message ;
}
}
2020-03-29 17:28:22 +02:00
public class PayjoinReceiverException : PayjoinException
{
2020-05-19 13:55:42 +02:00
public PayjoinReceiverException ( string errorCode , string receiverMessage ) : base ( FormatMessage ( errorCode ) )
2020-03-29 17:28:22 +02:00
{
ErrorCode = errorCode ;
2020-05-19 13:55:42 +02:00
ReceiverMessage = receiverMessage ;
WellknownError = PayjoinReceiverHelper . GetWellknownError ( errorCode ) ;
ErrorMessage = PayjoinReceiverHelper . GetMessage ( errorCode ) ;
2020-03-29 17:28:22 +02:00
}
public string ErrorCode { get ; }
public string ErrorMessage { get ; }
2020-05-19 13:55:42 +02:00
public string ReceiverMessage { get ; }
2020-03-29 17:28:22 +02:00
2020-05-09 12:29:05 +02:00
public PayjoinReceiverWellknownErrors ? WellknownError
2020-03-29 17:28:22 +02:00
{
2020-05-09 12:29:05 +02:00
get ;
}
2020-05-19 13:55:42 +02:00
private static string FormatMessage ( string errorCode )
2020-05-09 12:29:05 +02:00
{
2020-05-19 13:55:42 +02:00
return $"{errorCode}: {PayjoinReceiverHelper.GetMessage(errorCode)}" ;
2020-03-29 17:28:22 +02:00
}
}
public class PayjoinSenderException : PayjoinException
{
public PayjoinSenderException ( string message ) : base ( message )
{
}
}
}