2020-06-28 21:44:35 -05:00
using System ;
2020-03-30 00:28:22 +09:00
using System.Collections.Generic ;
2020-05-17 22:21:35 +09:00
using System.Globalization ;
2020-03-30 00:28:22 +09:00
using System.Linq ;
using System.Net.Http ;
using System.Text ;
using System.Threading ;
using System.Threading.Tasks ;
using NBitcoin ;
2020-06-17 21:43:56 +09:00
using NBitcoin.Payment ;
2020-03-30 00:28:22 +09:00
using Newtonsoft.Json ;
using Newtonsoft.Json.Linq ;
2020-04-09 17:38:55 +09:00
using IHttpClientFactory = System . Net . Http . IHttpClientFactory ;
2020-03-30 00:28:22 +09:00
namespace BTCPayServer.Services
{
2020-04-08 08:20:19 +02:00
public static class PSBTExtensions
{
2020-04-08 22:14:16 +09:00
public static ScriptPubKeyType ? GetInputsScriptPubKeyType ( this PSBT psbt )
2020-04-08 08:20:19 +02:00
{
2020-06-17 21:43:56 +09:00
if ( ! psbt . IsAllFinalized ( ) )
2020-04-08 22:14:16 +09:00
throw new InvalidOperationException ( "The psbt should be finalized with witness information" ) ;
var coinsPerTypes = psbt . Inputs . Select ( i = >
{
2020-04-09 19:44:16 +09:00
return ( ( PSBTCoin ) i , i . GetInputScriptPubKeyType ( ) ) ;
2020-04-08 22:14:16 +09: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 19:44:16 +09:00
public static ScriptPubKeyType ? GetInputScriptPubKeyType ( this PSBTInput i )
{
2020-06-17 21:43:56 +09:00
var scriptPubKey = i . GetTxOut ( ) . ScriptPubKey ;
if ( scriptPubKey . IsScriptType ( ScriptType . P2PKH ) )
return ScriptPubKeyType . Legacy ;
if ( scriptPubKey . IsScriptType ( ScriptType . P2WPKH ) )
2020-04-09 19:44:16 +09:00
return ScriptPubKeyType . Segwit ;
2020-06-17 21:43:56 +09:00
if ( scriptPubKey . IsScriptType ( ScriptType . P2SH ) & &
i . FinalScriptWitness is WitScript & &
2020-05-09 19:29:05 +09:00
PayToWitPubKeyHashTemplate . Instance . ExtractWitScriptParameters ( i . FinalScriptWitness ) is { } )
2020-04-09 19:44:16 +09:00
return ScriptPubKeyType . SegwitP2SH ;
2020-06-17 21:43:56 +09:00
if ( scriptPubKey . IsScriptType ( ScriptType . P2SH ) & &
i . RedeemScript is Script & &
PayToWitPubKeyHashTemplate . Instance . CheckScriptPubKey ( i . RedeemScript ) )
return ScriptPubKeyType . SegwitP2SH ;
2020-04-17 11:55:24 +02:00
return null ;
2020-04-09 19:44:16 +09:00
}
2020-04-08 08:20:19 +02:00
}
2020-05-17 05:07:24 +09:00
public class PayjoinClientParameters
{
2020-05-17 22:21:35 +09:00
public Money MaxAdditionalFeeContribution { get ; set ; }
public FeeRate MinFeeRate { get ; set ; }
public int? AdditionalFeeOutputIndex { get ; set ; }
2020-06-17 21:43:56 +09:00
public bool? DisableOutputSubstitution { get ; set ; }
2020-05-17 05:07:24 +09:00
public int Version { get ; set ; } = 1 ;
}
2020-03-30 00:28:22 +09:00
public class PayjoinClient
{
2020-04-09 17:38:55 +09: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-30 00:28:22 +09:00
private readonly ExplorerClientProvider _explorerClientProvider ;
2020-06-28 22:07:48 -05:00
private readonly IHttpClientFactory _httpClientFactory ;
2020-03-30 00:28:22 +09:00
2020-04-09 17:38:55 +09:00
public PayjoinClient ( ExplorerClientProvider explorerClientProvider , IHttpClientFactory httpClientFactory )
2020-03-30 00:28:22 +09:00
{
2020-05-09 19:29:05 +09:00
if ( httpClientFactory = = null )
throw new ArgumentNullException ( nameof ( httpClientFactory ) ) ;
2020-03-30 00:28:22 +09:00
_explorerClientProvider =
explorerClientProvider ? ? throw new ArgumentNullException ( nameof ( explorerClientProvider ) ) ;
2020-05-09 19:29:05 +09:00
_httpClientFactory = httpClientFactory ;
2020-03-30 00:28:22 +09:00
}
2020-05-17 05:07:24 +09:00
public Money MaxFeeBumpContribution { get ; set ; }
2020-05-17 22:21:35 +09:00
public FeeRate MinimumFeeRate { get ; set ; }
2020-05-17 05:07:24 +09:00
2020-06-17 21:43:56 +09:00
public async Task < PSBT > RequestPayjoin ( BitcoinUrlBuilder bip21 , DerivationSchemeSettings derivationSchemeSettings ,
PSBT signedPSBT , CancellationToken cancellationToken )
2020-03-30 00:28:22 +09:00
{
2020-06-17 21:43:56 +09:00
if ( bip21 = = null )
throw new ArgumentNullException ( nameof ( bip21 ) ) ;
if ( ! bip21 . TryGetPayjoinEndpoint ( out var endpoint ) )
throw new InvalidOperationException ( "This BIP21 does not support payjoin" ) ;
2020-05-09 19:29:05 +09:00
if ( derivationSchemeSettings = = null )
throw new ArgumentNullException ( nameof ( derivationSchemeSettings ) ) ;
2020-06-17 21:43:56 +09:00
if ( signedPSBT = = null )
throw new ArgumentNullException ( nameof ( signedPSBT ) ) ;
if ( signedPSBT . IsAllFinalized ( ) )
2020-04-08 18:24:04 +09:00
throw new InvalidOperationException ( "The original PSBT should not be finalized." ) ;
2020-06-17 21:43:56 +09:00
var optionalParameters = new PayjoinClientParameters ( ) ;
var inputScriptType = derivationSchemeSettings . AccountDerivation . ScriptPubKeyType ( ) ;
2020-03-30 00:28:22 +09:00
var signingAccount = derivationSchemeSettings . GetSigningAccountKeySettings ( ) ;
2020-06-17 21:43:56 +09:00
var paymentScriptPubKey = bip21 . Address ? . ScriptPubKey ;
var changeOutput = signedPSBT . Outputs . CoinsFor ( derivationSchemeSettings . AccountDerivation , signingAccount . AccountKey , signingAccount . GetRootedKeyPath ( ) )
. Where ( o = > o . ScriptPubKey ! = paymentScriptPubKey )
2020-05-17 05:07:24 +09:00
. FirstOrDefault ( ) ;
if ( changeOutput is PSBTOutput o )
2020-06-17 21:43:56 +09:00
optionalParameters . AdditionalFeeOutputIndex = ( int ) o . Index ;
if ( ! signedPSBT . TryGetEstimatedFeeRate ( out var originalFeeRate ) )
throw new ArgumentException ( "signedPSBT should have utxo information" , nameof ( signedPSBT ) ) ;
var originalFee = signedPSBT . GetFee ( ) ;
optionalParameters . MaxAdditionalFeeContribution = MaxFeeBumpContribution is null ?
// By default, we want to keep same fee rate and a single additional input
originalFeeRate . GetFee ( GetVirtualSize ( inputScriptType ) ) :
MaxFeeBumpContribution ;
2020-05-17 22:21:35 +09:00
if ( MinimumFeeRate is FeeRate v )
2020-06-17 21:43:56 +09:00
optionalParameters . MinFeeRate = v ;
bool allowOutputSubstitution = ! ( optionalParameters . DisableOutputSubstitution is true ) ;
if ( bip21 . UnknowParameters . TryGetValue ( "pjos" , out var pjos ) & & pjos = = "0" )
allowOutputSubstitution = false ;
PSBT originalPSBT = CreateOriginalPSBT ( signedPSBT ) ;
Transaction originalGlobalTx = signedPSBT . GetGlobalTransaction ( ) ;
TxOut feeOutput = changeOutput = = null ? null : originalGlobalTx . Outputs [ changeOutput . Index ] ;
var originalInputs = new Queue < ( TxIn OriginalTxIn , PSBTInput SignedPSBTInput ) > ( ) ;
for ( int i = 0 ; i < originalGlobalTx . Inputs . Count ; i + + )
2020-03-30 00:28:22 +09:00
{
2020-06-17 21:43:56 +09:00
originalInputs . Enqueue ( ( originalGlobalTx . Inputs [ i ] , signedPSBT . Inputs [ i ] ) ) ;
2020-03-30 00:28:22 +09:00
}
2020-06-17 21:43:56 +09:00
var originalOutputs = new Queue < ( TxOut OriginalTxOut , PSBTOutput SignedPSBTOutput ) > ( ) ;
for ( int i = 0 ; i < originalGlobalTx . Outputs . Count ; i + + )
2020-03-30 00:28:22 +09:00
{
2020-06-17 21:43:56 +09:00
originalOutputs . Enqueue ( ( originalGlobalTx . Outputs [ i ] , signedPSBT . Outputs [ i ] ) ) ;
2020-03-30 00:28:22 +09:00
}
2020-06-17 21:43:56 +09:00
endpoint = ApplyOptionalParameters ( endpoint , optionalParameters ) ;
var proposal = await SendOriginalTransaction ( endpoint , originalPSBT , cancellationToken ) ;
2020-03-30 00:28:22 +09:00
// Checking that the PSBT of the receiver is clean
2020-06-17 21:43:56 +09:00
if ( proposal . GlobalXPubs . Any ( ) )
2020-03-30 00:28:22 +09:00
{
throw new PayjoinSenderException ( "GlobalXPubs should not be included in the receiver's PSBT" ) ;
}
////////////
2020-06-17 21:43:56 +09:00
if ( proposal . CheckSanity ( ) is List < PSBTError > errors & & errors . Count > 0 )
throw new PayjoinSenderException ( $"The proposal PSBT is not sane ({errors[0]})" ) ;
var proposalGlobalTx = proposal . GetGlobalTransaction ( ) ;
// Verify that the transaction version, and nLockTime are unchanged.
if ( proposalGlobalTx . Version ! = originalGlobalTx . Version )
throw new PayjoinSenderException ( $"The proposal PSBT changed the transaction version" ) ;
if ( proposalGlobalTx . LockTime ! = originalGlobalTx . LockTime )
throw new PayjoinSenderException ( $"The proposal PSBT changed the nLocktime" ) ;
2020-03-30 00:28:22 +09:00
2020-06-17 21:43:56 +09:00
HashSet < Sequence > sequences = new HashSet < Sequence > ( ) ;
// For each inputs in the proposal:
foreach ( var proposedPSBTInput in proposal . Inputs )
2020-03-30 00:28:22 +09:00
{
2020-06-17 21:43:56 +09:00
if ( proposedPSBTInput . HDKeyPaths . Count ! = 0 )
throw new PayjoinSenderException ( "The receiver added keypaths to an input" ) ;
if ( proposedPSBTInput . PartialSigs . Count ! = 0 )
throw new PayjoinSenderException ( "The receiver added partial signatures to an input" ) ;
var proposedTxIn = proposalGlobalTx . Inputs . FindIndexedInput ( proposedPSBTInput . PrevOut ) . TxIn ;
bool isOurInput = originalInputs . Count > 0 & & originalInputs . Peek ( ) . OriginalTxIn . PrevOut = = proposedPSBTInput . PrevOut ;
// If it is one of our input
if ( isOurInput )
2020-03-30 00:28:22 +09:00
{
2020-06-17 21:43:56 +09:00
var input = originalInputs . Dequeue ( ) ;
// Verify that sequence is unchanged.
if ( input . OriginalTxIn . Sequence ! = proposedTxIn . Sequence )
throw new PayjoinSenderException ( "The proposedTxIn modified the sequence of one of our inputs" ) ;
// Verify the PSBT input is not finalized
if ( proposedPSBTInput . IsFinalized ( ) )
throw new PayjoinSenderException ( "The receiver finalized one of our inputs" ) ;
// Verify that <code>non_witness_utxo</code> and <code>witness_utxo</code> are not specified.
if ( proposedPSBTInput . NonWitnessUtxo ! = null | | proposedPSBTInput . WitnessUtxo ! = null )
throw new PayjoinSenderException ( "The receiver added non_witness_utxo or witness_utxo to one of our inputs" ) ;
sequences . Add ( proposedTxIn . Sequence ) ;
// Fill up the info from the original PSBT input so we can sign and get fees.
proposedPSBTInput . NonWitnessUtxo = input . SignedPSBTInput . NonWitnessUtxo ;
proposedPSBTInput . WitnessUtxo = input . SignedPSBTInput . WitnessUtxo ;
// We fill up information we had on the signed PSBT, so we can sign it.
foreach ( var hdKey in input . SignedPSBTInput . HDKeyPaths )
proposedPSBTInput . HDKeyPaths . Add ( hdKey . Key , hdKey . Value ) ;
proposedPSBTInput . RedeemScript = input . SignedPSBTInput . RedeemScript ;
2020-03-30 00:28:22 +09:00
}
else
{
2020-06-17 21:43:56 +09:00
// Verify the PSBT input is finalized
if ( ! proposedPSBTInput . IsFinalized ( ) )
throw new PayjoinSenderException ( "The receiver did not finalized one of their input" ) ;
// Verify that non_witness_utxo or witness_utxo are filled in.
if ( proposedPSBTInput . NonWitnessUtxo = = null & & proposedPSBTInput . WitnessUtxo = = null )
throw new PayjoinSenderException ( "The receiver did not specify non_witness_utxo or witness_utxo for one of their inputs" ) ;
sequences . Add ( proposedTxIn . Sequence ) ;
// Verify that the payjoin proposal did not introduced mixed input's type.
if ( inputScriptType ! = proposedPSBTInput . GetInputScriptPubKeyType ( ) )
throw new PayjoinSenderException ( "Mixed input type detected in the proposal" ) ;
2020-03-30 00:28:22 +09:00
}
}
2020-06-17 21:43:56 +09:00
// Verify that all of sender's inputs from the original PSBT are in the proposal.
if ( originalInputs . Count ! = 0 )
throw new PayjoinSenderException ( "Some of our inputs are not included in the proposal" ) ;
// Verify that the payjoin proposal did not introduced mixed input's sequence.
if ( sequences . Count ! = 1 )
throw new PayjoinSenderException ( "Mixed sequence detected in the proposal" ) ;
if ( ! proposal . TryGetFee ( out var newFee ) )
throw new PayjoinSenderException ( "The payjoin receiver did not included UTXO information to calculate fee correctly" ) ;
var additionalFee = newFee - originalFee ;
if ( additionalFee < Money . Zero )
throw new PayjoinSenderException ( "The receiver decreased absolute fee" ) ;
// For each outputs in the proposal:
foreach ( var proposedPSBTOutput in proposal . Outputs )
2020-03-30 00:28:22 +09:00
{
2020-06-17 21:43:56 +09:00
// Verify that no keypaths is in the PSBT output
if ( proposedPSBTOutput . HDKeyPaths . Count ! = 0 )
throw new PayjoinSenderException ( "The receiver added keypaths to an output" ) ;
bool isOriginalOutput = originalOutputs . Count > 0 & & originalOutputs . Peek ( ) . OriginalTxOut . ScriptPubKey = = proposedPSBTOutput . ScriptPubKey ;
if ( isOriginalOutput )
2020-04-06 23:21:02 +09:00
{
2020-06-17 21:43:56 +09:00
var originalOutput = originalOutputs . Dequeue ( ) ;
if ( originalOutput . OriginalTxOut = = feeOutput )
2020-04-09 19:44:16 +09:00
{
2020-06-17 21:43:56 +09:00
var actualContribution = feeOutput . Value - proposedPSBTOutput . Value ;
// The amount that was substracted from the output's value is less or equal to maxadditionalfeecontribution
if ( actualContribution > optionalParameters . MaxAdditionalFeeContribution )
throw new PayjoinSenderException ( "The actual contribution is more than maxadditionalfeecontribution" ) ;
// Make sure the actual contribution is only paying fee
if ( actualContribution > additionalFee )
throw new PayjoinSenderException ( "The actual contribution is not only paying fee" ) ;
// Make sure the actual contribution is only paying for fee incurred by additional inputs
var additionalInputsCount = proposalGlobalTx . Inputs . Count - originalGlobalTx . Inputs . Count ;
if ( actualContribution > originalFeeRate . GetFee ( GetVirtualSize ( inputScriptType ) ) * additionalInputsCount )
throw new PayjoinSenderException ( "The actual contribution is not only paying for additional inputs" ) ;
2020-04-09 19:44:16 +09:00
}
2020-06-17 21:43:56 +09:00
else if ( allowOutputSubstitution & &
originalOutput . OriginalTxOut . ScriptPubKey = = paymentScriptPubKey )
{
// That's the payment output, the receiver may have changed it.
}
else
{
if ( originalOutput . OriginalTxOut . Value > proposedPSBTOutput . Value )
throw new PayjoinSenderException ( "The receiver decreased the value of one of the outputs" ) ;
}
// We fill up information we had on the signed PSBT, so we can sign it.
foreach ( var hdKey in originalOutput . SignedPSBTOutput . HDKeyPaths )
proposedPSBTOutput . HDKeyPaths . Add ( hdKey . Key , hdKey . Value ) ;
proposedPSBTOutput . RedeemScript = originalOutput . SignedPSBTOutput . RedeemScript ;
}
}
// Verify that all of sender's outputs from the original PSBT are in the proposal.
if ( originalOutputs . Count ! = 0 )
{
if ( ! allowOutputSubstitution | |
originalOutputs . Count ! = 1 | |
originalOutputs . Dequeue ( ) . OriginalTxOut . ScriptPubKey ! = paymentScriptPubKey )
{
throw new PayjoinSenderException ( "Some of our outputs are not included in the proposal" ) ;
2020-04-06 23:21:02 +09:00
}
2020-03-30 00:28:22 +09:00
}
2020-06-17 21:43:56 +09:00
// If minfeerate was specified, check that the fee rate of the payjoin transaction is not less than this value.
if ( optionalParameters . MinFeeRate is FeeRate minFeeRate )
2020-05-17 22:21:35 +09:00
{
2020-06-17 21:43:56 +09:00
if ( ! proposal . TryGetEstimatedFeeRate ( out var newFeeRate ) )
throw new PayjoinSenderException ( "The payjoin receiver did not included UTXO information to calculate fee correctly" ) ;
2020-05-17 22:21:35 +09:00
if ( newFeeRate < minFeeRate )
throw new PayjoinSenderException ( "The payjoin receiver created a payjoin with a too low fee rate" ) ;
}
2020-06-17 21:43:56 +09:00
return proposal ;
}
2020-05-17 22:21:35 +09:00
2020-06-17 21:43:56 +09:00
private int GetVirtualSize ( ScriptPubKeyType ? scriptPubKeyType )
{
switch ( scriptPubKeyType )
2020-03-30 00:28:22 +09:00
{
2020-06-17 21:43:56 +09:00
case ScriptPubKeyType . Legacy :
return 148 ;
case ScriptPubKeyType . Segwit :
return 68 ;
case ScriptPubKeyType . SegwitP2SH :
return 91 ;
default :
return 110 ;
}
}
2020-04-06 23:21:02 +09:00
2020-06-17 21:43:56 +09:00
private static PSBT CreateOriginalPSBT ( PSBT signedPSBT )
{
var original = signedPSBT . Clone ( ) ;
original = original . Finalize ( ) ;
foreach ( var input in original . Inputs )
{
input . HDKeyPaths . Clear ( ) ;
input . PartialSigs . Clear ( ) ;
input . Unknown . Clear ( ) ;
}
foreach ( var output in original . Outputs )
{
output . Unknown . Clear ( ) ;
output . HDKeyPaths . Clear ( ) ;
2020-03-30 00:28:22 +09:00
}
2020-06-17 21:43:56 +09:00
original . GlobalXPubs . Clear ( ) ;
return original ;
}
private async Task < PSBT > SendOriginalTransaction ( Uri endpoint , PSBT originalTx , CancellationToken cancellationToken )
{
using ( HttpClient client = CreateHttpClient ( endpoint ) )
{
var bpuresponse = await client . PostAsync ( endpoint ,
2020-10-14 12:01:21 +02:00
new StringContent ( originalTx . ToBase64 ( ) , Encoding . UTF8 , "text/plain" ) , cancellationToken ) ;
2020-06-17 21:43:56 +09:00
if ( ! bpuresponse . IsSuccessStatusCode )
{
var errorStr = await bpuresponse . Content . ReadAsStringAsync ( ) ;
try
{
var error = JObject . Parse ( errorStr ) ;
throw new PayjoinReceiverException ( error [ "errorCode" ] . Value < string > ( ) ,
error [ "message" ] . Value < string > ( ) ) ;
}
catch ( JsonReaderException )
{
// will throw
bpuresponse . EnsureSuccessStatusCode ( ) ;
throw ;
}
}
2020-03-30 00:28:22 +09:00
2020-06-17 21:43:56 +09:00
var hex = await bpuresponse . Content . ReadAsStringAsync ( ) ;
return PSBT . Parse ( hex , originalTx . Network ) ;
}
2020-03-30 00:28:22 +09:00
}
2020-04-09 17:38:55 +09:00
2020-05-17 05:07:24 +09: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 22:21:35 +09:00
if ( clientParameters . AdditionalFeeOutputIndex is int additionalFeeOutputIndex )
parameters . Add ( $"additionalfeeoutputindex={additionalFeeOutputIndex.ToString(CultureInfo.InvariantCulture)}" ) ;
2020-06-17 21:43:56 +09:00
if ( clientParameters . DisableOutputSubstitution is bool disableoutputsubstitution )
parameters . Add ( $"disableoutputsubstitution={disableoutputsubstitution}" ) ;
2020-05-17 22:21:35 +09:00
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-17 05:07:24 +09:00
endpoint = new Uri ( $"{requestUri}?{string.Join('&', parameters)}" ) ;
return endpoint ;
}
2020-04-09 17:38:55 +09:00
private HttpClient CreateHttpClient ( Uri uri )
{
if ( uri . IsOnion ( ) )
return _httpClientFactory . CreateClient ( PayjoinOnionNamedClient ) ;
else
return _httpClientFactory . CreateClient ( PayjoinClearnetNamedClient ) ;
}
2020-03-30 00:28:22 +09:00
}
public class PayjoinException : Exception
{
public PayjoinException ( string message ) : base ( message )
{
}
}
2020-05-09 19:29:05 +09:00
public enum PayjoinReceiverWellknownErrors
{
Unavailable ,
NotEnoughMoney ,
2020-05-09 19:59:21 +09:00
VersionUnsupported ,
2020-05-28 13:35:48 +09:00
OriginalPSBTRejected
2020-05-09 19:29:05 +09:00
}
2020-05-19 20:55:42 +09: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 13:35:48 +09:00
yield return ( PayjoinReceiverWellknownErrors . OriginalPSBTRejected , "original-psbt-rejected" , "The receiver rejected the original PSBT." ) ;
2020-05-19 20:55:42 +09: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 ;
}
2020-06-28 22:07:48 -05:00
static readonly string UnknownError = "Unknown error from the receiver" ;
2020-05-19 20:55:42 +09:00
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-30 00:28:22 +09:00
public class PayjoinReceiverException : PayjoinException
{
2020-06-17 21:43:56 +09:00
public PayjoinReceiverException ( string errorCode , string receiverMessage ) : base ( FormatMessage ( errorCode , receiverMessage ) )
2020-03-30 00:28:22 +09:00
{
ErrorCode = errorCode ;
2020-05-19 20:55:42 +09:00
ReceiverMessage = receiverMessage ;
WellknownError = PayjoinReceiverHelper . GetWellknownError ( errorCode ) ;
ErrorMessage = PayjoinReceiverHelper . GetMessage ( errorCode ) ;
2020-03-30 00:28:22 +09:00
}
public string ErrorCode { get ; }
public string ErrorMessage { get ; }
2020-05-19 20:55:42 +09:00
public string ReceiverMessage { get ; }
2020-03-30 00:28:22 +09:00
2020-05-09 19:29:05 +09:00
public PayjoinReceiverWellknownErrors ? WellknownError
2020-03-30 00:28:22 +09:00
{
2020-05-09 19:29:05 +09:00
get ;
}
2020-06-17 21:43:56 +09:00
private static string FormatMessage ( string errorCode , string receiverMessage )
2020-05-09 19:29:05 +09:00
{
2020-06-17 21:43:56 +09:00
return $"{errorCode}: {PayjoinReceiverHelper.GetMessage(errorCode)}. (Receiver message: {receiverMessage})" ;
2020-03-30 00:28:22 +09:00
}
}
public class PayjoinSenderException : PayjoinException
{
public PayjoinSenderException ( string message ) : base ( message )
{
}
}
}