using System; using System.Collections.Generic; using System.Linq; using NBitcoin; using NBXplorer.DerivationStrategy; namespace BTCPayServer { public class DerivationSchemeParser { public BTCPayNetwork BtcPayNetwork { get; } public Network Network => BtcPayNetwork.NBitcoinNetwork; public Script HintScriptPubKey { get; set; } public DerivationSchemeParser(BTCPayNetwork expectedNetwork) { if (expectedNetwork == null) throw new ArgumentNullException(nameof(expectedNetwork)); BtcPayNetwork = expectedNetwork; } 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(); var hintDestination = HintScriptPubKey?.GetDestination(); if (hintDestination != null) { if (hintDestination is KeyId) { hintedLabels.Add("legacy"); } if (hintDestination is ScriptId) { hintedLabels.Add("p2sh"); } } if (!Network.Consensus.SupportSegwit) { hintedLabels.Add("legacy"); str = str.Replace("-[p2sh]", string.Empty, StringComparison.OrdinalIgnoreCase); } try { var result = BtcPayNetwork.NBXplorerNetwork.DerivationStrategyFactory.Parse(str); return FindMatch(hintedLabels, result); } 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; } } if (hintDestination != null) { if (hintDestination is WitKeyId) { hintedLabels.Remove("legacy"); hintedLabels.Remove("p2sh"); } } str = string.Join('-', parts.Where(p => !IsLabel(p))); foreach (var label in hintedLabels) { str = $"{str}-[{label}]"; } return FindMatch(hintedLabels, 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 DerivationStrategyBase FindMatch(HashSet hintLabels, DerivationStrategyBase result) { var firstKeyPath = new KeyPath("0/0"); if (HintScriptPubKey == null) return result; if (HintScriptPubKey == result.GetDerivation(firstKeyPath).ScriptPubKey) return result; if (result is MultisigDerivationStrategy) hintLabels.Add("keeporder"); var resultNoLabels = result.ToString(); resultNoLabels = string.Join('-', resultNoLabels.Split('-').Where(p => !IsLabel(p))); foreach (var labels in ItemCombinations(hintLabels.ToList())) { var hinted = BtcPayNetwork.NBXplorerNetwork.DerivationStrategyFactory.Parse(resultNoLabels + '-' + string.Join('-', labels.Select(l => $"[{l}]").ToArray())); if (HintScriptPubKey == hinted.GetDerivation(firstKeyPath).ScriptPubKey) return hinted; } throw new FormatException("Could not find any match"); } 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; } } }