diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index f8afc495f..93049c656 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -2121,7 +2121,7 @@ namespace BTCPayServer.Tests .IsType(await controller.AddDerivationScheme(user.StoreId, "BTC")).Model; string content = "{\"keystore\": {\"ckcc_xpub\": \"xpub661MyMwAqRbcGVBsTGeNZN6QGVHmMHLdSA4FteGsRrEriu4pnVZMZWnruFFFXkMnyoBjyHndD3Qwcfz4MPzBUxjSevweNFQx7SAYZATtcDw\", \"xpub\": \"ypub6WWc2gWwHbdnAAyJDnR4SPL1phRh7REqrPBfZeizaQ1EmTshieRXJC3Z5YoU4wkcdKHEjQGkh6AYEzCQC1Kz3DNaWSwdc1pc8416hAjzqyD\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/49'/0'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}"; - derivationVM.ColdcardPublicFile = TestUtils.GetFormFile("wallet.json", content); + derivationVM.ElectrumWalletFile = TestUtils.GetFormFile("wallet.json", content); derivationVM = (DerivationSchemeViewModel)Assert.IsType(controller .AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult()).Model; Assert.False(derivationVM @@ -2132,7 +2132,7 @@ namespace BTCPayServer.Tests "{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"upub5DBYp1qGgsTrkzCptMGZc2x18pquLwGrBw6nS59T4NViZ4cni1mGowQzziy85K8vzkp1jVtWrSkLhqk9KDfvrGeB369wGNYf39kX8rQfiLn\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/49'/0'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}"; derivationVM = (DerivationSchemeViewModel)Assert .IsType(await controller.AddDerivationScheme(user.StoreId, "BTC")).Model; - derivationVM.ColdcardPublicFile = TestUtils.GetFormFile("wallet2.json", content); + derivationVM.ElectrumWalletFile = TestUtils.GetFormFile("wallet2.json", content); derivationVM.Enabled = true; derivationVM = (DerivationSchemeViewModel)Assert.IsType(controller .AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult()).Model; @@ -2144,7 +2144,7 @@ namespace BTCPayServer.Tests var store = tester.PayTester.StoreRepository.FindStore(user.StoreId).GetAwaiter().GetResult(); var onchainBTC = store.GetSupportedPaymentMethods(tester.PayTester.Networks) .OfType().First(o => o.PaymentId.IsBTCOnChain); - DerivationSchemeSettings.TryParseFromColdcard(content, onchainBTC.Network, out var expected); + DerivationSchemeSettings.TryParseFromElectrumWallet(content, onchainBTC.Network, out var expected); Assert.Equal(expected.ToJson(), onchainBTC.ToJson()); // Let's check that the root hdkey and account key path are taken into account when making a PSBT @@ -3448,7 +3448,7 @@ normal: var root = new Mnemonic( "usage fever hen zero slide mammal silent heavy donate budget pulse say brain thank sausage brand craft about save attract muffin advance illegal cabbage") .DeriveExtKey(); - Assert.True(DerivationSchemeSettings.TryParseFromColdcard( + Assert.True(DerivationSchemeSettings.TryParseFromElectrumWallet( "{\"keystore\": {\"ckcc_xpub\": \"xpub661MyMwAqRbcGVBsTGeNZN6QGVHmMHLdSA4FteGsRrEriu4pnVZMZWnruFFFXkMnyoBjyHndD3Qwcfz4MPzBUxjSevweNFQx7SAYZATtcDw\", \"xpub\": \"ypub6WWc2gWwHbdnAAyJDnR4SPL1phRh7REqrPBfZeizaQ1EmTshieRXJC3Z5YoU4wkcdKHEjQGkh6AYEzCQC1Kz3DNaWSwdc1pc8416hAjzqyD\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/49'/0'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}", mainnet, out var settings)); Assert.Equal(root.GetPublicKey().GetHDFingerPrint(), settings.AccountKeySettings[0].RootFingerprint); @@ -3465,20 +3465,20 @@ normal: var testnet = new BTCPayNetworkProvider(NetworkType.Testnet).GetNetwork("BTC"); // Should be legacy - Assert.True(DerivationSchemeSettings.TryParseFromColdcard( + Assert.True(DerivationSchemeSettings.TryParseFromElectrumWallet( "{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"tpubDDWYqT3P24znfsaGX7kZcQhNc5LAjnQiKQvUCHF2jS6dsgJBRtymopEU5uGpMaR5YChjuiExZG1X2aTbqXkp82KqH5qnqwWHp6EWis9ZvKr\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/44'/1'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}", testnet, out settings)); Assert.True(settings.AccountDerivation is DirectDerivationStrategy s && !s.Segwit); // Should be segwit p2sh - Assert.True(DerivationSchemeSettings.TryParseFromColdcard( + Assert.True(DerivationSchemeSettings.TryParseFromElectrumWallet( "{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"upub5DSddA9NoRUyJrQ4p86nsCiTSY7kLHrSxx3joEJXjHd4HPARhdXUATuk585FdWPVC2GdjsMePHb6BMDmf7c6KG4K4RPX6LVqBLtDcWpQJmh\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/49'/1'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}", testnet, out settings)); Assert.True(settings.AccountDerivation is P2SHDerivationStrategy p && p.Inner is DirectDerivationStrategy s2 && s2.Segwit); // Should be segwit - Assert.True(DerivationSchemeSettings.TryParseFromColdcard( + Assert.True(DerivationSchemeSettings.TryParseFromElectrumWallet( "{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"vpub5YjYxTemJ39tFRnuAhwduyxG2tKGjoEpmvqVQRPqdYrqa6YGoeSzBtHXaJUYB19zDbXs3JjbEcVWERjQBPf9bEfUUMZNMv1QnMyHV8JPqyf\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/84'/1'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}", testnet, out settings)); Assert.True(settings.AccountDerivation is DirectDerivationStrategy s3 && s3.Segwit); diff --git a/BTCPayServer/Controllers/StoresController.BTCLike.cs b/BTCPayServer/Controllers/StoresController.BTCLike.cs index 7fd83cf65..c5174aa0c 100644 --- a/BTCPayServer/Controllers/StoresController.BTCLike.cs +++ b/BTCPayServer/Controllers/StoresController.BTCLike.cs @@ -110,14 +110,14 @@ namespace BTCPayServer.Controllers } } - if (vm.ColdcardPublicFile != null) + if (vm.ElectrumWalletFile != null) { - if (!DerivationSchemeSettings.TryParseFromColdcard(await ReadAllText(vm.ColdcardPublicFile), network, out strategy)) + if (!DerivationSchemeSettings.TryParseFromElectrumWallet(await ReadAllText(vm.ElectrumWalletFile), network, out strategy)) { TempData.SetStatusMessageModel(new StatusMessageModel() { Severity = StatusMessageModel.StatusSeverity.Error, - Message = "Coldcard public file was not in the correct format" + Message = "Electrum wallet/Air-gapped hardware wallet file was not in the correct format" }); vm.Confirmation = false; return View(nameof(AddDerivationScheme),vm); diff --git a/BTCPayServer/DerivationSchemeSettings.cs b/BTCPayServer/DerivationSchemeSettings.cs index 2b86683e9..32e522e39 100644 --- a/BTCPayServer/DerivationSchemeSettings.cs +++ b/BTCPayServer/DerivationSchemeSettings.cs @@ -38,7 +38,26 @@ namespace BTCPayServer return strategy != null; } - public static bool TryParseFromColdcard(string coldcardExport, BTCPayNetwork network, out DerivationSchemeSettings settings) + private static bool TryParseXpub(string xpub, DerivationSchemeParser derivationSchemeParser, ref DerivationSchemeSettings derivationSchemeSettings) + { + try + { + derivationSchemeSettings.AccountOriginal = xpub.Trim(); + derivationSchemeSettings.AccountDerivation = derivationSchemeParser.ParseElectrum(derivationSchemeSettings.AccountOriginal); + derivationSchemeSettings.AccountKeySettings = new AccountKeySettings[1]; + derivationSchemeSettings.AccountKeySettings[0] = new AccountKeySettings(); + derivationSchemeSettings.AccountKeySettings[0].AccountKey = derivationSchemeSettings.AccountDerivation.GetExtPubKeys().Single().GetWif(derivationSchemeParser.Network); + 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; + } + catch (Exception e) + { + return false; + } + } + + public static bool TryParseFromElectrumWallet(string coldcardExport, BTCPayNetwork network, out DerivationSchemeSettings settings) { settings = null; if (coldcardExport == null) @@ -46,7 +65,7 @@ namespace BTCPayServer if (network == null) throw new ArgumentNullException(nameof(network)); var result = new DerivationSchemeSettings(); - result.Source = "Coldcard"; + result.Source = "Electrum/Airgap hardware wallet"; var derivationSchemeParser = new DerivationSchemeParser(network); JObject jobj = null; try @@ -56,27 +75,11 @@ namespace BTCPayServer } catch { - return false; + return TryParseXpub(coldcardExport, derivationSchemeParser, ref result); } - if (jobj.ContainsKey("xpub")) - { - try - { - result.AccountOriginal = jobj["xpub"].Value().Trim(); - result.AccountDerivation = derivationSchemeParser.ParseElectrum(result.AccountOriginal); - result.AccountKeySettings = new AccountKeySettings[1]; - result.AccountKeySettings[0] = new AccountKeySettings(); - result.AccountKeySettings[0].AccountKey = result.AccountDerivation.GetExtPubKeys().Single().GetWif(network.NBitcoinNetwork); - if (result.AccountDerivation is DirectDerivationStrategy direct && !direct.Segwit) - result.AccountOriginal = null; // Saving this would be confusing for user, as xpub of electrum is legacy derivation, but for btcpay, it is segwit derivation - } - catch - { - return false; - } - } - else + if (!jobj.ContainsKey("xpub") || + !TryParseXpub(jobj["xpub"].Value(), derivationSchemeParser, ref result)) { return false; } diff --git a/BTCPayServer/Models/StoreViewModels/DerivationSchemeViewModel.cs b/BTCPayServer/Models/StoreViewModels/DerivationSchemeViewModel.cs index bd8bc1847..629da7244 100644 --- a/BTCPayServer/Models/StoreViewModels/DerivationSchemeViewModel.cs +++ b/BTCPayServer/Models/StoreViewModels/DerivationSchemeViewModel.cs @@ -35,8 +35,8 @@ namespace BTCPayServer.Models.StoreViewModels public KeyPath RootKeyPath { get; set; } - [Display(Name = "Coldcard Wallet File")] - public IFormFile ColdcardPublicFile{ get; set; } + [Display(Name = "Electrum Wallet File")] + public IFormFile ElectrumWalletFile{ get; set; } public string Config { get; set; } public string Source { get; set; } public string DerivationSchemeFormat { get; set; } diff --git a/BTCPayServer/Views/Stores/AddDerivationScheme.cshtml b/BTCPayServer/Views/Stores/AddDerivationScheme.cshtml index 66315de6d..3c3d50138 100644 --- a/BTCPayServer/Views/Stores/AddDerivationScheme.cshtml +++ b/BTCPayServer/Views/Stores/AddDerivationScheme.cshtml @@ -85,7 +85,7 @@ { } - + @if (Model.CanUseHotWallet) { diff --git a/BTCPayServer/Views/Stores/AddDerivationSchemes_HardwareWalletDialogs.cshtml b/BTCPayServer/Views/Stores/AddDerivationSchemes_HardwareWalletDialogs.cshtml index 603a92d64..60e35af9b 100644 --- a/BTCPayServer/Views/Stores/AddDerivationSchemes_HardwareWalletDialogs.cshtml +++ b/BTCPayServer/Views/Stores/AddDerivationSchemes_HardwareWalletDialogs.cshtml @@ -7,21 +7,26 @@ } -