diff --git a/BTCPayServer.Tests/FastTests.cs b/BTCPayServer.Tests/FastTests.cs index 038063957..32252a42d 100644 --- a/BTCPayServer.Tests/FastTests.cs +++ b/BTCPayServer.Tests/FastTests.cs @@ -958,6 +958,40 @@ namespace BTCPayServer.Tests Assert.Equal("49'/0'/0'", specter.AccountKeySettings[0].AccountKeyPath.ToString()); Assert.Equal("Specter", specter.Label); Assert.Null(error); + + //BSMS BIP129, Nunchuk + + var bsms = @"BSMS 1.0 +wsh(sortedmulti(1,[5c9e228d/48'/0'/0'/2']xpub6EgGHjcvovyN3nK921zAGPfuB41cJXkYRdt3tLGmiMyvbgHpss4X1eRZwShbEBb1znz2e2bCkCED87QZpin3sSYKbmCzQ9Sc7LaV98ngdeX/**,[2b0e251e/48'/0'/0'/2']xpub6DrimHB8KUSkPvmJ8Pk8RE769EdDm2VEoZ8MBz76w9QupP8Py4wexs4Pa3aRB1LUEhc9GyY6ypDWEFFRCgqeDQePcyWQfjtmintrehq3JCL/**)) +/0/*,/1/* +bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku +"; + + Assert.True(DerivationSchemeSettings.TryParseFromWalletFile(bsms, + mainnet, out var nunchuk, out error)); + + Assert.Equal(2, nunchuk.AccountKeySettings.Length); + //check that the account key settings match those in bsms string + Assert.Equal("5c9e228d", nunchuk.AccountKeySettings[0].RootFingerprint.ToString()); + Assert.Equal("48'/0'/0'/2'", nunchuk.AccountKeySettings[0].AccountKeyPath.ToString()); +Assert.Equal("2b0e251e", nunchuk.AccountKeySettings[1].RootFingerprint.ToString()); + Assert.Equal("48'/0'/0'/2'", nunchuk.AccountKeySettings[1].AccountKeyPath.ToString()); + + var multsig = Assert.IsType < MultisigDerivationStrategy > + (Assert.IsType(nunchuk.AccountDerivation).Inner); + + Assert.True(multsig.LexicographicOrder); + Assert.Equal(1, multsig.RequiredSignatures); + + var deposit = new NBXplorer.KeyPathTemplates(null).GetKeyPathTemplate(DerivationFeature.Deposit); + var line =nunchuk.AccountDerivation.GetLineFor(deposit).Derive(0); + + Assert.Equal(BitcoinAddress.Create("bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku", Network.Main).ScriptPubKey, + line.ScriptPubKey); + + Assert.Equal("BSMS", nunchuk.Source); + Assert.Null(error); + // Failure case Assert.False(DerivationSchemeSettings.TryParseFromWalletFile( diff --git a/BTCPayServer/DerivationSchemeSettings.cs b/BTCPayServer/DerivationSchemeSettings.cs index 026c9f122..02eb7b570 100644 --- a/BTCPayServer/DerivationSchemeSettings.cs +++ b/BTCPayServer/DerivationSchemeSettings.cs @@ -121,6 +121,65 @@ namespace BTCPayServer } } + public static bool TryParseBSMSFile(string filecontent, DerivationSchemeParser derivationSchemeParser, ref DerivationSchemeSettings derivationSchemeSettings, + out string error) + { + error = null; + try + { + string[] lines = filecontent.Split( + new[] {"\r\n", "\r", "\n"}, + StringSplitOptions.None + ); + + if (!lines[0].Trim().Equals("BSMS 1.0")) + {; + return false; + } + + var descriptor = lines[1]; + var derivationPath = lines[2].Split(',', StringSplitOptions.RemoveEmptyEntries).FirstOrDefault()?? "/0/*"; + if (derivationPath == "No path restrictions") + { + derivationPath = "/0/*"; + } + if(derivationPath != "/0/*") + { + error = "BTCPay Server can only derive address to the deposit and change paths"; + return false; + } + + + descriptor = descriptor.Replace("/**", derivationPath); + var testAddress = BitcoinAddress.Create( lines[3], derivationSchemeParser.Network); + var result = derivationSchemeParser.ParseOutputDescriptor(descriptor); + + var deposit = new NBXplorer.KeyPathTemplates(null).GetKeyPathTemplate(DerivationFeature.Deposit); + var line = result.Item1.GetLineFor(deposit).Derive(0); + + if (testAddress.ScriptPubKey != line.ScriptPubKey) + { + error = "BSMS test address did not match our generated address"; + return false; + } + + derivationSchemeSettings.Source = "BSMS"; + derivationSchemeSettings.AccountDerivation = result.Item1; + derivationSchemeSettings.AccountOriginal = descriptor.Trim(); + 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 e) + { + error = $"BSMS parse error: {e.Message}"; + return false; + } + } public static bool TryParseFromWalletFile(string fileContents, BTCPayNetwork network, out DerivationSchemeSettings settings, out string error) { settings = null; @@ -140,6 +199,17 @@ namespace BTCPayServer } catch { + if (TryParseBSMSFile(fileContents, derivationSchemeParser,ref result, out var bsmsError)) + { + settings = result; + settings.Network = network; + return true; + } + if (bsmsError is not null) + { + error = bsmsError; + return false; + } result.Source = "GenericFile"; if (TryParseXpub(fileContents, derivationSchemeParser, ref result, ref error) || TryParseXpub(fileContents, derivationSchemeParser, ref result, ref error, false)) diff --git a/BTCPayServer/Views/UIStores/ImportWallet/File.cshtml b/BTCPayServer/Views/UIStores/ImportWallet/File.cshtml index 2bb9d629c..4bda655fb 100644 --- a/BTCPayServer/Views/UIStores/ImportWallet/File.cshtml +++ b/BTCPayServer/Views/UIStores/ImportWallet/File.cshtml @@ -56,6 +56,10 @@ Passport Wallet ❯ Connect Wallet ❯ BTCPay ❯ microSD ❯ Save wallet file + + + Nunchuk + ... ❯ Export wallet configuration