using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using NBitcoin; using NBitcoin.DataEncoders; using NBXplorer; using NBXplorer.DerivationStrategy; namespace BTCPayServer { public class DerivationSchemeParser { public Network Network { get; set; } public Script HintScriptPubKey { get; set; } public DerivationSchemeParser(Network expectedNetwork) { Network = expectedNetwork; } 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"); try { var result = new DerivationStrategyFactory(Network).Parse(str); return FindMatch(hintedLabels, result); } catch { } Dictionary electrumMapping = new Dictionary(); //Source https://github.com/spesmilo/electrum/blob/9edffd17542de5773e7284a8c8a2673c766bb3c3/lib/bitcoin.py var standard = 0x0488b21eU; electrumMapping.Add(standard, new[] { "legacy" }); var p2wpkh_p2sh = 0x049d7cb2U; electrumMapping.Add(p2wpkh_p2sh, new string[] { "p2sh" }); var p2wpkh = 0x4b24746U; electrumMapping.Add(p2wpkh, Array.Empty()); var parts = str.Split('-'); for (int i = 0; i < parts.Length; i++) { if (IsLabel(parts[i])) { hintedLabels.Add(parts[i].Substring(1, parts[i].Length - 2).ToLowerInvariant()); continue; } try { var data = Encoders.Base58Check.DecodeData(parts[i]); if (data.Length < 4) continue; var prefix = Utils.ToUInt32(data, false); var standardPrefix = Utils.ToBytes(Network.NetworkType == NetworkType.Mainnet ? 0x0488b21eU : 0x043587cf, false); for (int ii = 0; ii < 4; ii++) data[ii] = standardPrefix[ii]; var derivationScheme = new BitcoinExtPubKey(Encoders.Base58Check.EncodeData(data), Network).ToString(); electrumMapping.TryGetValue(prefix, out string[] labels); if (labels != null) { foreach (var label in labels) { hintedLabels.Add(label.ToLowerInvariant()); } } 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, new DerivationStrategyFactory(Network).Parse(str)); } private DerivationStrategyBase FindMatch(HashSet hintLabels, DerivationStrategyBase result) { var facto = new DerivationStrategyFactory(Network); var firstKeyPath = new KeyPath("0/0"); if (HintScriptPubKey == null) return result; if (HintScriptPubKey == result.Derive(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 = facto.Parse(resultNoLabels + '-' + string.Join('-', labels.Select(l=>$"[{l}]").ToArray())); if (HintScriptPubKey == hinted.Derive(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; } } }