using System; using System.Collections.Generic; using System.Linq; using NBitcoin; using NBitcoin.Scripting; using NBXplorer.DerivationStrategy; namespace BTCPayServer { public class DerivationSchemeParser { public BTCPayNetwork BtcPayNetwork { get; } public Network Network => BtcPayNetwork.NBitcoinNetwork; public DerivationSchemeParser(BTCPayNetwork expectedNetwork) { if (expectedNetwork == null) throw new ArgumentNullException(nameof(expectedNetwork)); BtcPayNetwork = expectedNetwork; } public (DerivationStrategyBase, RootedKeyPath[]) ParseOutputDescriptor(string str) { (DerivationStrategyBase, RootedKeyPath[]) ExtractFromPkProvider(PubKeyProvider pubKeyProvider, string suffix = "") { switch (pubKeyProvider) { case PubKeyProvider.Const _: throw new FormatException("Only HD output descriptors are supported."); case PubKeyProvider.HD hd: if (hd.Path != null && hd.Path.ToString() != "0") { throw new FormatException("Custom change paths are not supported."); } return (Parse($"{hd.Extkey}{suffix}"), null); case PubKeyProvider.Origin origin: var innerResult = ExtractFromPkProvider(origin.Inner, suffix); return (innerResult.Item1, new[] {origin.KeyOriginInfo}); default: throw new ArgumentOutOfRangeException(); } } if (str == null) throw new ArgumentNullException(nameof(str)); str = str.Trim(); var outputDescriptor = OutputDescriptor.Parse(str, Network); switch(outputDescriptor) { case OutputDescriptor.PK _: case OutputDescriptor.Raw _: case OutputDescriptor.Addr _: throw new FormatException("Only HD output descriptors are supported."); case OutputDescriptor.Combo _: throw new FormatException("Only output descriptors of one format are supported."); case OutputDescriptor.Multi multi: var xpubs = multi.PkProviders.Select(provider => ExtractFromPkProvider(provider)); return ( Parse( $"{multi.Threshold}-of-{(string.Join('-', xpubs.Select(tuple => tuple.Item1.ToString())))}{(multi.IsSorted?"":"-[keeporder]")}"), xpubs.SelectMany(tuple => tuple.Item2).ToArray()); case OutputDescriptor.PKH pkh: return ExtractFromPkProvider(pkh.PkProvider, "-[legacy]"); case OutputDescriptor.SH sh: var suffix = "-[p2sh]"; if (sh.Inner is OutputDescriptor.Multi) { //non segwit suffix = "-[legacy]"; } if (sh.Inner is OutputDescriptor.Multi || sh.Inner is OutputDescriptor.WPKH || sh.Inner is OutputDescriptor.WSH) { var ds = ParseOutputDescriptor(sh.Inner.ToString()); return (Parse(ds.Item1 + suffix), ds.Item2); }; throw new FormatException("sh descriptors are only supported with multsig(legacy or p2wsh) and segwit(p2wpkh)"); case OutputDescriptor.WPKH wpkh: return ExtractFromPkProvider(wpkh.PkProvider, ""); case OutputDescriptor.WSH wsh: if (wsh.Inner is OutputDescriptor.Multi) { return ParseOutputDescriptor(wsh.Inner.ToString()); } throw new FormatException("wsh descriptors are only supported with multisig"); default: throw new ArgumentOutOfRangeException(nameof(outputDescriptor)); } } public DerivationStrategyBase ParseElectrum(string str) { if (str == null) throw new ArgumentNullException(nameof(str)); str = str.Trim(); var data = Network.GetBase58CheckEncoder().DecodeData(str); if (data.Length < 4) throw new FormatException(); var prefix = Utils.ToUInt32(data, false); var standardPrefix = Utils.ToBytes(0x0488b21eU, false); for (int ii = 0; ii < 4; ii++) data[ii] = standardPrefix[ii]; var extPubKey = GetBitcoinExtPubKeyByNetwork(Network, data); if (!BtcPayNetwork.ElectrumMapping.TryGetValue(prefix, out var type)) { throw new FormatException(); } if (type == DerivationType.Segwit) return new DirectDerivationStrategy(extPubKey, true); if (type == DerivationType.Legacy) return new DirectDerivationStrategy(extPubKey, false); if (type == DerivationType.SegwitP2SH) return BtcPayNetwork.NBXplorerNetwork.DerivationStrategyFactory.Parse(extPubKey.ToString() + "-[p2sh]"); throw new FormatException(); } public DerivationStrategyBase Parse(string str) { if (str == null) throw new ArgumentNullException(nameof(str)); str = str.Trim(); HashSet hintedLabels = new HashSet(); if (!Network.Consensus.SupportSegwit) { hintedLabels.Add("legacy"); str = str.Replace("-[p2sh]", string.Empty, StringComparison.OrdinalIgnoreCase); } try { return BtcPayNetwork.NBXplorerNetwork.DerivationStrategyFactory.Parse(str); } catch { } var parts = str.Split('-'); bool hasLabel = false; for (int i = 0; i < parts.Length; i++) { if (IsLabel(parts[i])) { if (!hasLabel) { hintedLabels.Clear(); if (!Network.Consensus.SupportSegwit) hintedLabels.Add("legacy"); } hasLabel = true; hintedLabels.Add(parts[i].Substring(1, parts[i].Length - 2).ToLowerInvariant()); continue; } try { var data = Network.GetBase58CheckEncoder().DecodeData(parts[i]); if (data.Length < 4) continue; var prefix = Utils.ToUInt32(data, false); var standardPrefix = Utils.ToBytes(0x0488b21eU, false); for (int ii = 0; ii < 4; ii++) data[ii] = standardPrefix[ii]; var derivationScheme = GetBitcoinExtPubKeyByNetwork(Network, data).ToString(); if (BtcPayNetwork.ElectrumMapping.TryGetValue(prefix, out var type)) { switch (type) { case DerivationType.Legacy: hintedLabels.Add("legacy"); break; case DerivationType.SegwitP2SH: hintedLabels.Add("p2sh"); break; } } parts[i] = derivationScheme; } catch { continue; } } str = string.Join('-', parts.Where(p => !IsLabel(p))); foreach (var label in hintedLabels) { str = $"{str}-[{label}]"; } return BtcPayNetwork.NBXplorerNetwork.DerivationStrategyFactory.Parse(str); } public static BitcoinExtPubKey GetBitcoinExtPubKeyByNetwork(Network network, byte[] data) { try { return new BitcoinExtPubKey(network.GetBase58CheckEncoder().EncodeData(data), network.NetworkSet.Mainnet).ToNetwork(network); } catch (Exception) { return new BitcoinExtPubKey(network.GetBase58CheckEncoder().EncodeData(data), Network.Main).ToNetwork(network); } } private static bool IsLabel(string v) { return v.StartsWith('[') && v.EndsWith(']'); } /// /// Method to create lists containing possible combinations of an input list of items. This is /// basically copied from code by user "jaolho" on this thread: /// http://stackoverflow.com/questions/7802822/all-possible-combinations-of-a-list-of-values /// /// type of the items on the input list /// list of items /// minimum number of items wanted in the generated combinations, /// if zero the empty combination is included, /// default is one /// maximum number of items wanted in the generated combinations, /// default is no maximum limit /// list of lists for possible combinations of the input items public static List> ItemCombinations(List inputList, int minimumItems = 1, int maximumItems = int.MaxValue) { int nonEmptyCombinations = (int)Math.Pow(2, inputList.Count) - 1; List> listOfLists = new List>(nonEmptyCombinations + 1); if (minimumItems == 0) // Optimize default case listOfLists.Add(new List()); for (int i = 1; i <= nonEmptyCombinations; i++) { List thisCombination = new List(inputList.Count); for (int j = 0; j < inputList.Count; j++) { if ((i >> j & 1) == 1) thisCombination.Add(inputList[j]); } if (thisCombination.Count >= minimumItems && thisCombination.Count <= maximumItems) listOfLists.Add(thisCombination); } return listOfLists; } } }