2020-06-29 04:44:35 +02:00
using System ;
2019-05-08 16:39:11 +02:00
using System.Collections.Generic ;
using System.Linq ;
2021-05-07 17:35:43 +02:00
using System.Text ;
2019-05-08 16:39:11 +02:00
using BTCPayServer.Payments ;
using NBitcoin ;
2020-06-22 08:39:29 +02:00
using NBitcoin.DataEncoders ;
2019-05-08 16:39:11 +02:00
using NBXplorer.DerivationStrategy ;
using Newtonsoft.Json ;
using Newtonsoft.Json.Linq ;
namespace BTCPayServer
{
public class DerivationSchemeSettings : ISupportedPaymentMethod
{
public static DerivationSchemeSettings Parse ( string derivationStrategy , BTCPayNetwork network )
{
2021-12-28 09:39:54 +01:00
ArgumentNullException . ThrowIfNull ( network ) ;
ArgumentNullException . ThrowIfNull ( derivationStrategy ) ;
2021-01-11 03:22:42 +01:00
var result = new DerivationSchemeSettings ( ) ;
result . Network = network ;
var parser = new DerivationSchemeParser ( network ) ;
if ( TryParseXpub ( derivationStrategy , parser , ref result , false ) | | TryParseXpub ( derivationStrategy , parser , ref result , true ) )
{
return result ;
}
throw new FormatException ( "Invalid Derivation Scheme" ) ;
2019-05-08 16:39:11 +02:00
}
2019-05-08 17:40:30 +02:00
2019-05-09 09:11:09 +02:00
public static bool TryParseFromJson ( string config , BTCPayNetwork network , out DerivationSchemeSettings strategy )
{
2021-12-28 09:39:54 +01:00
ArgumentNullException . ThrowIfNull ( network ) ;
ArgumentNullException . ThrowIfNull ( config ) ;
2019-05-09 09:11:09 +02:00
strategy = null ;
try
{
strategy = network . NBXplorerNetwork . Serializer . ToObject < DerivationSchemeSettings > ( config ) ;
strategy . Network = network ;
}
catch { }
return strategy ! = null ;
}
2020-06-22 08:39:29 +02:00
private static bool TryParseXpub ( string xpub , DerivationSchemeParser derivationSchemeParser , ref DerivationSchemeSettings derivationSchemeSettings , bool electrum = true )
2020-05-25 15:32:06 +02:00
{
2021-01-11 03:22:42 +01:00
if ( ! electrum )
{
try
{
var result = derivationSchemeParser . ParseOutputDescriptor ( xpub ) ;
derivationSchemeSettings . AccountOriginal = xpub . Trim ( ) ;
derivationSchemeSettings . AccountDerivation = result . Item1 ;
derivationSchemeSettings . AccountKeySettings = result . Item2 . Select ( ( path , i ) = > new AccountKeySettings ( )
{
RootFingerprint = path ? . MasterFingerprint ,
AccountKeyPath = path ? . KeyPath ,
AccountKey = result . Item1 . GetExtPubKeys ( ) . ElementAt ( i ) . GetWif ( derivationSchemeParser . Network )
} ) . ToArray ( ) ;
return true ;
}
catch ( Exception )
{
// ignored
}
}
2020-05-25 15:32:06 +02:00
try
{
derivationSchemeSettings . AccountOriginal = xpub . Trim ( ) ;
2020-06-22 08:39:29 +02:00
derivationSchemeSettings . AccountDerivation = electrum ? derivationSchemeParser . ParseElectrum ( derivationSchemeSettings . AccountOriginal ) : derivationSchemeParser . Parse ( derivationSchemeSettings . AccountOriginal ) ;
2021-01-11 14:47:32 +01:00
derivationSchemeSettings . AccountKeySettings = derivationSchemeSettings . AccountDerivation . GetExtPubKeys ( )
. Select ( key = > new AccountKeySettings ( )
{
AccountKey = key . GetWif ( derivationSchemeParser . Network )
} ) . ToArray ( ) ;
2020-05-25 15:32:06 +02:00
if ( derivationSchemeSettings . AccountDerivation is DirectDerivationStrategy direct & & ! direct . Segwit )
derivationSchemeSettings . AccountOriginal = null ; // Saving this would be confusing for user, as xpub of electrum is legacy derivation, but for btcpay, it is segwit derivation
return true ;
}
2020-06-08 16:40:58 +02:00
catch ( Exception )
2020-05-25 15:32:06 +02:00
{
return false ;
}
}
2020-06-28 10:55:27 +02:00
2020-06-22 08:39:29 +02:00
public static bool TryParseFromWalletFile ( string fileContents , BTCPayNetwork network , out DerivationSchemeSettings settings )
2019-05-08 17:40:30 +02:00
{
settings = null ;
2021-12-28 09:39:54 +01:00
ArgumentNullException . ThrowIfNull ( fileContents ) ;
ArgumentNullException . ThrowIfNull ( network ) ;
2019-05-08 17:40:30 +02:00
var result = new DerivationSchemeSettings ( ) ;
2019-05-09 09:05:18 +02:00
var derivationSchemeParser = new DerivationSchemeParser ( network ) ;
2019-05-08 17:40:30 +02:00
JObject jobj = null ;
try
{
2021-05-07 17:35:43 +02:00
if ( HexEncoder . IsWellFormed ( fileContents ) )
{
fileContents = Encoding . UTF8 . GetString ( Encoders . Hex . DecodeData ( fileContents ) ) ;
}
2020-06-22 08:39:29 +02:00
jobj = JObject . Parse ( fileContents ) ;
2019-05-08 17:40:30 +02:00
}
catch
{
2020-06-22 08:39:29 +02:00
result . Source = "GenericFile" ;
2020-11-24 09:54:17 +01:00
if ( TryParseXpub ( fileContents , derivationSchemeParser , ref result ) | |
TryParseXpub ( fileContents , derivationSchemeParser , ref result , false ) )
{
settings = result ;
settings . Network = network ;
return true ;
}
2020-12-04 07:41:02 +01:00
return false ;
2019-05-08 17:40:30 +02:00
}
2021-02-03 16:33:47 +01:00
// Electrum
2020-06-22 08:39:29 +02:00
if ( jobj . ContainsKey ( "keystore" ) )
2019-05-08 17:40:30 +02:00
{
2020-06-22 08:39:29 +02:00
result . Source = "ElectrumFile" ;
jobj = ( JObject ) jobj [ "keystore" ] ;
2020-06-28 10:55:27 +02:00
2020-06-22 08:39:29 +02:00
if ( ! jobj . ContainsKey ( "xpub" ) | |
! TryParseXpub ( jobj [ "xpub" ] . Value < string > ( ) , derivationSchemeParser , ref result ) )
{
return false ;
}
2019-05-08 17:40:30 +02:00
2020-06-22 08:39:29 +02:00
if ( jobj . ContainsKey ( "label" ) )
2019-05-08 17:40:30 +02:00
{
2020-06-22 08:39:29 +02:00
try
{
result . Label = jobj [ "label" ] . Value < string > ( ) ;
}
catch { return false ; }
2019-05-08 17:40:30 +02:00
}
2020-06-22 08:39:29 +02:00
if ( jobj . ContainsKey ( "ckcc_xfp" ) )
2019-05-08 17:40:30 +02:00
{
2020-06-22 08:39:29 +02:00
try
{
result . AccountKeySettings [ 0 ] . RootFingerprint = new HDFingerprint ( jobj [ "ckcc_xfp" ] . Value < uint > ( ) ) ;
}
catch { return false ; }
2019-05-08 17:40:30 +02:00
}
2020-06-22 08:39:29 +02:00
if ( jobj . ContainsKey ( "derivation" ) )
{
try
{
result . AccountKeySettings [ 0 ] . AccountKeyPath = new KeyPath ( jobj [ "derivation" ] . Value < string > ( ) ) ;
}
catch { return false ; }
}
}
2021-02-03 16:33:47 +01:00
// Specter
else if ( jobj . ContainsKey ( "descriptor" ) & & jobj . ContainsKey ( "blockheight" ) )
{
result . Source = "SpecterFile" ;
if ( ! TryParseXpub ( jobj [ "descriptor" ] . Value < string > ( ) , derivationSchemeParser , ref result , false ) )
{
return false ;
}
if ( jobj . ContainsKey ( "label" ) )
{
try
{
result . Label = jobj [ "label" ] . Value < string > ( ) ;
}
catch { return false ; }
}
}
// Wasabi
2020-06-22 08:39:29 +02:00
else
2019-05-08 17:40:30 +02:00
{
2020-06-22 08:39:29 +02:00
result . Source = "WasabiFile" ;
if ( ! jobj . ContainsKey ( "ExtPubKey" ) | |
! TryParseXpub ( jobj [ "ExtPubKey" ] . Value < string > ( ) , derivationSchemeParser , ref result , false ) )
{
return false ;
}
if ( jobj . ContainsKey ( "MasterFingerprint" ) )
{
try
{
2020-06-28 10:55:27 +02:00
var mfpString = jobj [ "MasterFingerprint" ] . ToString ( ) . Trim ( ) ;
2020-06-22 08:39:29 +02:00
// https://github.com/zkSNACKs/WalletWasabi/pull/1663#issuecomment-508073066
2020-06-28 10:55:27 +02:00
if ( uint . TryParse ( mfpString , out var fingerprint ) )
2020-06-22 08:39:29 +02:00
{
result . AccountKeySettings [ 0 ] . RootFingerprint = new HDFingerprint ( fingerprint ) ;
}
else
{
var shouldReverseMfp = jobj . ContainsKey ( "ColdCardFirmwareVersion" ) & &
jobj [ "ColdCardFirmwareVersion" ] . ToString ( ) = = "2.1.0" ;
var bytes = Encoders . Hex . DecodeData ( mfpString ) ;
result . AccountKeySettings [ 0 ] . RootFingerprint = shouldReverseMfp ? new HDFingerprint ( bytes . Reverse ( ) . ToArray ( ) ) : new HDFingerprint ( bytes ) ;
}
}
2020-06-28 10:55:27 +02:00
2020-06-22 08:39:29 +02:00
catch { return false ; }
}
if ( jobj . ContainsKey ( "AccountKeyPath" ) )
{
try
{
result . AccountKeySettings [ 0 ] . AccountKeyPath = new KeyPath ( jobj [ "AccountKeyPath" ] . Value < string > ( ) ) ;
}
catch { return false ; }
}
if ( jobj . ContainsKey ( "DerivationPath" ) )
{
try
{
result . AccountKeySettings [ 0 ] . AccountKeyPath = new KeyPath ( jobj [ "DerivationPath" ] . Value < string > ( ) . ToLowerInvariant ( ) ) ;
}
catch { return false ; }
}
if ( jobj . ContainsKey ( "ColdCardFirmwareVersion" ) )
{
result . Source = "ColdCard" ;
}
if ( jobj . ContainsKey ( "CoboVaultFirmwareVersion" ) )
2019-05-08 17:40:30 +02:00
{
2020-06-22 08:39:29 +02:00
result . Source = "CoboVault" ;
2019-05-08 17:40:30 +02:00
}
}
settings = result ;
settings . Network = network ;
return true ;
}
2019-05-08 16:39:11 +02:00
public DerivationSchemeSettings ( )
{
}
2019-05-12 17:30:28 +02:00
2019-05-08 16:39:11 +02:00
public DerivationSchemeSettings ( DerivationStrategyBase derivationStrategy , BTCPayNetwork network )
{
2021-12-28 09:39:54 +01:00
ArgumentNullException . ThrowIfNull ( network ) ;
ArgumentNullException . ThrowIfNull ( derivationStrategy ) ;
2019-05-08 16:39:11 +02:00
AccountDerivation = derivationStrategy ;
Network = network ;
2019-05-12 17:13:55 +02:00
AccountKeySettings = derivationStrategy . GetExtPubKeys ( ) . Select ( c = > new AccountKeySettings ( )
{
AccountKey = c . GetWif ( network . NBitcoinNetwork )
} ) . ToArray ( ) ;
2019-05-08 16:39:11 +02:00
}
2019-05-12 17:30:28 +02:00
BitcoinExtPubKey _SigningKey ;
public BitcoinExtPubKey SigningKey
{
get
{
return _SigningKey ? ? AccountKeySettings ? . Select ( k = > k . AccountKey ) . FirstOrDefault ( ) ;
}
set
{
_SigningKey = value ;
}
}
2019-05-08 16:39:11 +02:00
[JsonIgnore]
public BTCPayNetwork Network { get ; set ; }
2019-05-08 17:54:53 +02:00
public string Source { get ; set ; }
2021-06-17 08:36:22 +02:00
public bool IsHotWallet { get ; set ; }
2019-05-12 17:13:55 +02:00
2021-02-11 11:48:54 +01:00
[Obsolete("Use GetSigningAccountKeySettings().AccountKeyPath instead")]
2019-05-12 17:13:55 +02:00
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
2019-05-08 16:39:11 +02:00
public KeyPath AccountKeyPath { get ; set ; }
2019-05-10 12:30:10 +02:00
2019-05-08 16:39:11 +02:00
public DerivationStrategyBase AccountDerivation { get ; set ; }
public string AccountOriginal { get ; set ; }
2021-02-11 11:48:54 +01:00
[Obsolete("Use GetSigningAccountKeySettings().RootFingerprint instead")]
2019-05-12 17:13:55 +02:00
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
2019-05-08 16:39:11 +02:00
public HDFingerprint ? RootFingerprint { get ; set ; }
2021-02-11 11:48:54 +01:00
[Obsolete("Use GetSigningAccountKeySettings().AccountKey instead")]
2019-05-12 17:13:55 +02:00
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
2019-05-10 12:30:10 +02:00
public BitcoinExtPubKey ExplicitAccountKey { get ; set ; }
[JsonIgnore]
2021-02-11 11:48:54 +01:00
[Obsolete("Use GetSigningAccountKeySettings().AccountKey instead")]
2019-05-10 12:30:10 +02:00
public BitcoinExtPubKey AccountKey
{
get
{
return ExplicitAccountKey ? ? new BitcoinExtPubKey ( AccountDerivation . GetExtPubKeys ( ) . First ( ) , Network . NBitcoinNetwork ) ;
}
}
2019-05-12 17:30:28 +02:00
public AccountKeySettings GetSigningAccountKeySettings ( )
{
return AccountKeySettings . Single ( a = > a . AccountKey = = SigningKey ) ;
}
2019-05-12 17:13:55 +02:00
AccountKeySettings [ ] _AccountKeySettings ;
public AccountKeySettings [ ] AccountKeySettings
{
get
{
// Legacy
if ( _AccountKeySettings = = null )
{
if ( this . Network = = null )
return null ;
_AccountKeySettings = AccountDerivation . GetExtPubKeys ( ) . Select ( e = > new AccountKeySettings ( )
{
AccountKey = e . GetWif ( this . Network . NBitcoinNetwork ) ,
} ) . ToArray ( ) ;
#pragma warning disable CS0618 // Type or member is obsolete
_AccountKeySettings [ 0 ] . AccountKeyPath = AccountKeyPath ;
_AccountKeySettings [ 0 ] . RootFingerprint = RootFingerprint ;
ExplicitAccountKey = null ;
AccountKeyPath = null ;
RootFingerprint = null ;
#pragma warning restore CS0618 // Type or member is obsolete
}
return _AccountKeySettings ;
}
set
{
_AccountKeySettings = value ;
}
}
2019-05-12 04:07:41 +02:00
public IEnumerable < NBXplorer . Models . PSBTRebaseKeyRules > GetPSBTRebaseKeyRules ( )
{
2019-05-12 17:30:28 +02:00
foreach ( var accountKey in AccountKeySettings )
2019-05-12 04:07:41 +02:00
{
2019-11-16 09:22:51 +01:00
if ( accountKey . GetRootedKeyPath ( ) is RootedKeyPath rootedKeyPath )
2019-05-12 04:07:41 +02:00
{
2019-05-12 17:13:55 +02:00
yield return new NBXplorer . Models . PSBTRebaseKeyRules ( )
{
AccountKey = accountKey . AccountKey ,
2019-11-16 09:22:51 +01:00
AccountKeyPath = rootedKeyPath
2019-05-12 17:13:55 +02:00
} ;
}
2019-05-12 04:07:41 +02:00
}
}
2019-05-08 16:39:11 +02:00
public string Label { get ; set ; }
2019-05-08 18:06:03 +02:00
[JsonIgnore]
2019-05-08 16:39:11 +02:00
public PaymentMethodId PaymentId = > new PaymentMethodId ( Network . CryptoCode , PaymentTypes . BTCLike ) ;
public override string ToString ( )
{
return AccountDerivation . ToString ( ) ;
}
public string ToPrettyString ( )
{
2019-05-08 18:07:05 +02:00
return ! string . IsNullOrEmpty ( Label ) ? Label :
! String . IsNullOrEmpty ( AccountOriginal ) ? AccountOriginal :
2019-05-08 16:39:11 +02:00
ToString ( ) ;
}
2019-05-09 09:11:09 +02:00
public string ToJson ( )
{
return Network . NBXplorerNetwork . Serializer . ToString ( this ) ;
}
2019-05-12 04:07:41 +02:00
public void RebaseKeyPaths ( PSBT psbt )
{
foreach ( var rebase in GetPSBTRebaseKeyRules ( ) )
{
2019-11-16 09:22:51 +01:00
psbt . RebaseKeyPaths ( rebase . AccountKey , rebase . AccountKeyPath ) ;
2019-05-12 04:07:41 +02:00
}
}
2019-05-08 16:39:11 +02:00
}
2019-05-12 17:13:55 +02:00
public class AccountKeySettings
{
public HDFingerprint ? RootFingerprint { get ; set ; }
public KeyPath AccountKeyPath { get ; set ; }
2019-05-14 09:06:43 +02:00
public RootedKeyPath GetRootedKeyPath ( )
{
if ( RootFingerprint is HDFingerprint fp & & AccountKeyPath ! = null )
return new RootedKeyPath ( fp , AccountKeyPath ) ;
return null ;
}
2019-05-12 17:13:55 +02:00
public BitcoinExtPubKey AccountKey { get ; set ; }
public bool IsFullySetup ( )
{
return AccountKeyPath ! = null & & RootFingerprint is HDFingerprint ;
}
}
2019-05-08 16:39:11 +02:00
}