Support wasabi file format input (#1671)

* Support wasabi file format input

This adds support to importing the wasabi file format which seems to be used more for imports by Wasabi,ColdCard,BlueWallet & Cobo Vault (and way better than electrum anyway)

* fixes

* add test
This commit is contained in:
Andrew Camilleri
2020-06-22 08:39:29 +02:00
committed by GitHub
parent e751be21ea
commit 0dd1b668cd
6 changed files with 149 additions and 53 deletions

View File

@@ -2249,12 +2249,40 @@ namespace BTCPayServer.Tests
.AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult()).Model; .AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult()).Model;
Assert.True(derivationVM.Confirmation); Assert.True(derivationVM.Confirmation);
//cobo vault file
var content = "{\"ExtPubKey\":\"xpub6CEqRFZ7yZxCFXuEWZBAdnC8bdvu9SRHevaoU2SsW9ZmKhrCShmbpGZWwaR15hdLURf8hg47g4TpPGaqEU8hw5LEJCE35AUhne67XNyFGBk\",\"MasterFingerprint\":\"7a7563b5\",\"DerivationPath\":\"M\\/84'\\/0'\\/0'\",\"CoboVaultFirmwareVersion\":\"1.2.0(BTC-Only)\"}";
derivationVM = (DerivationSchemeViewModel)Assert
.IsType<ViewResult>(await controller.AddDerivationScheme(user.StoreId, "BTC")).Model;
derivationVM.WalletFile = TestUtils.GetFormFile("wallet3.json", content);
derivationVM.Enabled = true;
derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(controller
.AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult()).Model;
Assert.True(derivationVM.Confirmation);
Assert.IsType<RedirectToActionResult>(controller.AddDerivationScheme(user.StoreId, derivationVM, "BTC")
.GetAwaiter().GetResult());
//wasabi wallet file
content =
"{\r\n \"EncryptedSecret\": \"6PYWBQ1zsukowsnTNA57UUx791aBuJusm7E4egXUmF5WGw3tcdG3cmTL57\",\r\n \"ChainCode\": \"waSIVbn8HaoovoQg/0t8IS1+ZCxGsJRGFT21i06nWnc=\",\r\n \"MasterFingerprint\": \"7a7563b5\",\r\n \"ExtPubKey\": \"xpub6CEqRFZ7yZxCFXuEWZBAdnC8bdvu9SRHevaoU2SsW9ZmKhrCShmbpGZWwaR15hdLURf8hg47g4TpPGaqEU8hw5LEJCE35AUhne67XNyFGBk\",\r\n \"PasswordVerified\": false,\r\n \"MinGapLimit\": 21,\r\n \"AccountKeyPath\": \"84'/0'/0'\",\r\n \"BlockchainState\": {\r\n \"Network\": \"RegTest\",\r\n \"Height\": \"0\"\r\n },\r\n \"HdPubKeys\": []\r\n}";
derivationVM = (DerivationSchemeViewModel)Assert
.IsType<ViewResult>(await controller.AddDerivationScheme(user.StoreId, "BTC")).Model;
derivationVM.WalletFile = TestUtils.GetFormFile("wallet4.json", content);
derivationVM.Enabled = true;
derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(controller
.AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult()).Model;
Assert.True(derivationVM.Confirmation);
Assert.IsType<RedirectToActionResult>(controller.AddDerivationScheme(user.StoreId, derivationVM, "BTC")
.GetAwaiter().GetResult());
// Can we upload coldcard settings? (Should fail, we are giving a mainnet file to a testnet network) // Can we upload coldcard settings? (Should fail, we are giving a mainnet file to a testnet network)
derivationVM = (DerivationSchemeViewModel)Assert derivationVM = (DerivationSchemeViewModel)Assert
.IsType<ViewResult>(await controller.AddDerivationScheme(user.StoreId, "BTC")).Model; .IsType<ViewResult>(await controller.AddDerivationScheme(user.StoreId, "BTC")).Model;
string content = 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}"; "{\"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.ElectrumWalletFile = TestUtils.GetFormFile("wallet.json", content); derivationVM.WalletFile = TestUtils.GetFormFile("wallet.json", content);
derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(controller derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(controller
.AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult()).Model; .AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult()).Model;
Assert.False(derivationVM Assert.False(derivationVM
@@ -2265,7 +2293,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}"; "{\"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 derivationVM = (DerivationSchemeViewModel)Assert
.IsType<ViewResult>(await controller.AddDerivationScheme(user.StoreId, "BTC")).Model; .IsType<ViewResult>(await controller.AddDerivationScheme(user.StoreId, "BTC")).Model;
derivationVM.ElectrumWalletFile = TestUtils.GetFormFile("wallet2.json", content); derivationVM.WalletFile = TestUtils.GetFormFile("wallet2.json", content);
derivationVM.Enabled = true; derivationVM.Enabled = true;
derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(controller derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(controller
.AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult()).Model; .AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult()).Model;
@@ -2273,11 +2301,12 @@ namespace BTCPayServer.Tests
Assert.IsType<RedirectToActionResult>(controller.AddDerivationScheme(user.StoreId, derivationVM, "BTC") Assert.IsType<RedirectToActionResult>(controller.AddDerivationScheme(user.StoreId, derivationVM, "BTC")
.GetAwaiter().GetResult()); .GetAwaiter().GetResult());
// Now let's check that no data has been lost in the process // Now let's check that no data has been lost in the process
var store = tester.PayTester.StoreRepository.FindStore(user.StoreId).GetAwaiter().GetResult(); var store = tester.PayTester.StoreRepository.FindStore(user.StoreId).GetAwaiter().GetResult();
var onchainBTC = store.GetSupportedPaymentMethods(tester.PayTester.Networks) var onchainBTC = store.GetSupportedPaymentMethods(tester.PayTester.Networks)
.OfType<DerivationSchemeSettings>().First(o => o.PaymentId.IsBTCOnChain); .OfType<DerivationSchemeSettings>().First(o => o.PaymentId.IsBTCOnChain);
DerivationSchemeSettings.TryParseFromElectrumWallet(content, onchainBTC.Network, out var expected); DerivationSchemeSettings.TryParseFromWalletFile(content, onchainBTC.Network, out var expected);
Assert.Equal(expected.ToJson(), onchainBTC.ToJson()); 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 // Let's check that the root hdkey and account key path are taken into account when making a PSBT
@@ -3581,7 +3610,7 @@ normal:
var root = new Mnemonic( 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") "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(); .DeriveExtKey();
Assert.True(DerivationSchemeSettings.TryParseFromElectrumWallet( Assert.True(DerivationSchemeSettings.TryParseFromWalletFile(
"{\"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}", "{\"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)); mainnet, out var settings));
Assert.Equal(root.GetPublicKey().GetHDFingerPrint(), settings.AccountKeySettings[0].RootFingerprint); Assert.Equal(root.GetPublicKey().GetHDFingerPrint(), settings.AccountKeySettings[0].RootFingerprint);
@@ -3598,20 +3627,20 @@ normal:
var testnet = new BTCPayNetworkProvider(NetworkType.Testnet).GetNetwork<BTCPayNetwork>("BTC"); var testnet = new BTCPayNetworkProvider(NetworkType.Testnet).GetNetwork<BTCPayNetwork>("BTC");
// Should be legacy // Should be legacy
Assert.True(DerivationSchemeSettings.TryParseFromElectrumWallet( Assert.True(DerivationSchemeSettings.TryParseFromWalletFile(
"{\"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}", "{\"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)); testnet, out settings));
Assert.True(settings.AccountDerivation is DirectDerivationStrategy s && !s.Segwit); Assert.True(settings.AccountDerivation is DirectDerivationStrategy s && !s.Segwit);
// Should be segwit p2sh // Should be segwit p2sh
Assert.True(DerivationSchemeSettings.TryParseFromElectrumWallet( Assert.True(DerivationSchemeSettings.TryParseFromWalletFile(
"{\"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}", "{\"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)); testnet, out settings));
Assert.True(settings.AccountDerivation is P2SHDerivationStrategy p && Assert.True(settings.AccountDerivation is P2SHDerivationStrategy p &&
p.Inner is DirectDerivationStrategy s2 && s2.Segwit); p.Inner is DirectDerivationStrategy s2 && s2.Segwit);
// Should be segwit // Should be segwit
Assert.True(DerivationSchemeSettings.TryParseFromElectrumWallet( Assert.True(DerivationSchemeSettings.TryParseFromWalletFile(
"{\"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}", "{\"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)); testnet, out settings));
Assert.True(settings.AccountDerivation is DirectDerivationStrategy s3 && s3.Segwit); Assert.True(settings.AccountDerivation is DirectDerivationStrategy s3 && s3.Segwit);

View File

@@ -110,14 +110,14 @@ namespace BTCPayServer.Controllers
} }
} }
if (vm.ElectrumWalletFile != null) if (vm.WalletFile != null)
{ {
if (!DerivationSchemeSettings.TryParseFromElectrumWallet(await ReadAllText(vm.ElectrumWalletFile), network, out strategy)) if (!DerivationSchemeSettings.TryParseFromWalletFile(await ReadAllText(vm.WalletFile), network, out strategy))
{ {
TempData.SetStatusMessageModel(new StatusMessageModel() TempData.SetStatusMessageModel(new StatusMessageModel()
{ {
Severity = StatusMessageModel.StatusSeverity.Error, Severity = StatusMessageModel.StatusSeverity.Error,
Message = "Electrum wallet/Air-gapped hardware wallet file was not in the correct format" Message = "Wallet file was not in the correct format"
}); });
vm.Confirmation = false; vm.Confirmation = false;
return View(nameof(AddDerivationScheme),vm); return View(nameof(AddDerivationScheme),vm);

View File

@@ -1,9 +1,11 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Payments; using BTCPayServer.Payments;
using NBitcoin; using NBitcoin;
using NBitcoin.DataEncoders;
using NBXplorer.DerivationStrategy; using NBXplorer.DerivationStrategy;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
@@ -38,12 +40,12 @@ namespace BTCPayServer
return strategy != null; return strategy != null;
} }
private static bool TryParseXpub(string xpub, DerivationSchemeParser derivationSchemeParser, ref DerivationSchemeSettings derivationSchemeSettings) private static bool TryParseXpub(string xpub, DerivationSchemeParser derivationSchemeParser, ref DerivationSchemeSettings derivationSchemeSettings, bool electrum = true)
{ {
try try
{ {
derivationSchemeSettings.AccountOriginal = xpub.Trim(); derivationSchemeSettings.AccountOriginal = xpub.Trim();
derivationSchemeSettings.AccountDerivation = derivationSchemeParser.ParseElectrum(derivationSchemeSettings.AccountOriginal); derivationSchemeSettings.AccountDerivation = electrum ? derivationSchemeParser.ParseElectrum(derivationSchemeSettings.AccountOriginal) : derivationSchemeParser.Parse(derivationSchemeSettings.AccountOriginal);
derivationSchemeSettings.AccountKeySettings = new AccountKeySettings[1]; derivationSchemeSettings.AccountKeySettings = new AccountKeySettings[1];
derivationSchemeSettings.AccountKeySettings[0] = new AccountKeySettings(); derivationSchemeSettings.AccountKeySettings[0] = new AccountKeySettings();
derivationSchemeSettings.AccountKeySettings[0].AccountKey = derivationSchemeSettings.AccountDerivation.GetExtPubKeys().Single().GetWif(derivationSchemeParser.Network); derivationSchemeSettings.AccountKeySettings[0].AccountKey = derivationSchemeSettings.AccountDerivation.GetExtPubKeys().Single().GetWif(derivationSchemeParser.Network);
@@ -57,27 +59,32 @@ namespace BTCPayServer
} }
} }
public static bool TryParseFromElectrumWallet(string coldcardExport, BTCPayNetwork network, out DerivationSchemeSettings settings) public static bool TryParseFromWalletFile(string fileContents, BTCPayNetwork network, out DerivationSchemeSettings settings)
{ {
settings = null; settings = null;
if (coldcardExport == null) if (fileContents == null)
throw new ArgumentNullException(nameof(coldcardExport)); throw new ArgumentNullException(nameof(fileContents));
if (network == null) if (network == null)
throw new ArgumentNullException(nameof(network)); throw new ArgumentNullException(nameof(network));
var result = new DerivationSchemeSettings(); var result = new DerivationSchemeSettings();
result.Source = "Electrum/Airgap hardware wallet";
var derivationSchemeParser = new DerivationSchemeParser(network); var derivationSchemeParser = new DerivationSchemeParser(network);
JObject jobj = null; JObject jobj = null;
try try
{ {
jobj = JObject.Parse(coldcardExport); jobj = JObject.Parse(fileContents);
jobj = (JObject)jobj["keystore"];
} }
catch catch
{ {
return TryParseXpub(coldcardExport, derivationSchemeParser, ref result); result.Source = "GenericFile";
return TryParseXpub(fileContents, derivationSchemeParser, ref result);
} }
//electrum
if (jobj.ContainsKey("keystore"))
{
result.Source = "ElectrumFile";
jobj = (JObject)jobj["keystore"];
if (!jobj.ContainsKey("xpub") || if (!jobj.ContainsKey("xpub") ||
!TryParseXpub(jobj["xpub"].Value<string>(), derivationSchemeParser, ref result)) !TryParseXpub(jobj["xpub"].Value<string>(), derivationSchemeParser, ref result))
{ {
@@ -110,6 +117,65 @@ namespace BTCPayServer
} }
catch { return false; } catch { return false; }
} }
}
else
{
result.Source = "WasabiFile";
//wasabi format
if (!jobj.ContainsKey("ExtPubKey") ||
!TryParseXpub(jobj["ExtPubKey"].Value<string>(), derivationSchemeParser, ref result, false))
{
return false;
}
if (jobj.ContainsKey("MasterFingerprint"))
{
try
{
var mfpString = jobj["MasterFingerprint"].ToString().Trim();
// https://github.com/zkSNACKs/WalletWasabi/pull/1663#issuecomment-508073066
if(uint.TryParse(mfpString, out var fingerprint))
{
result.AccountKeySettings[0].RootFingerprint = new HDFingerprint(fingerprint);
}
else
{
var shouldReverseMfp = jobj.ContainsKey("ColdCardFirmwareVersion") &&
jobj["ColdCardFirmwareVersion"].ToString() == "2.1.0";
var bytes = Encoders.Hex.DecodeData(mfpString);
result.AccountKeySettings[0].RootFingerprint = shouldReverseMfp ? new HDFingerprint(bytes.Reverse().ToArray()) : new HDFingerprint(bytes);
}
}
catch { return false; }
}
if (jobj.ContainsKey("AccountKeyPath"))
{
try
{
result.AccountKeySettings[0].AccountKeyPath = new KeyPath(jobj["AccountKeyPath"].Value<string>());
}
catch { return false; }
}
if (jobj.ContainsKey("DerivationPath"))
{
try
{
result.AccountKeySettings[0].AccountKeyPath = new KeyPath(jobj["DerivationPath"].Value<string>().ToLowerInvariant());
}
catch { return false; }
}
if (jobj.ContainsKey("ColdCardFirmwareVersion"))
{
result.Source = "ColdCard";
}
if (jobj.ContainsKey("CoboVaultFirmwareVersion"))
{
result.Source = "CoboVault";
}
}
settings = result; settings = result;
settings.Network = network; settings.Network = network;
return true; return true;

View File

@@ -35,8 +35,8 @@ namespace BTCPayServer.Models.StoreViewModels
public KeyPath RootKeyPath { get; set; } public KeyPath RootKeyPath { get; set; }
[Display(Name = "Electrum Wallet File")] [Display(Name = "Electrum/Hardware Wallet File")]
public IFormFile ElectrumWalletFile{ get; set; } public IFormFile WalletFile{ get; set; }
public string Config { get; set; } public string Config { get; set; }
public string Source { get; set; } public string Source { get; set; }
public string DerivationSchemeFormat { get; set; } public string DerivationSchemeFormat { get; set; }

View File

@@ -85,7 +85,7 @@
{ {
<button class="dropdown-item check-for-vault" type="button">... a hardware wallet</button> <button class="dropdown-item check-for-vault" type="button">... a hardware wallet</button>
} }
<button class="dropdown-item" type="button" data-toggle="modal" data-target="#electrumimport">... Electrum/Air-gapped hardware wallet file</button> <button class="dropdown-item" type="button" data-toggle="modal" data-target="#electrumimport">... a wallet file (Electrum, Wasabi, Cobo Vault, ColdCard)</button>
@if (Model.CanUseHotWallet) @if (Model.CanUseHotWallet)
{ {
<button class="dropdown-item" data-toggle="modal" data-target="#nbxplorergeneratewallet" type="button" id="nbxplorergeneratewalletbtn">... a new/existing seed.</button> <button class="dropdown-item" data-toggle="modal" data-target="#nbxplorergeneratewallet" type="button" id="nbxplorergeneratewalletbtn">... a new/existing seed.</button>

View File

@@ -11,7 +11,7 @@
<div class="modal-dialog modal-lg" role="document"> <div class="modal-dialog modal-lg" role="document">
<form class="modal-content" form method="post" enctype="multipart/form-data"> <form class="modal-content" form method="post" enctype="multipart/form-data">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title" id="electrumimportLabel">Import Electrum/Air-gapped Hardware Wallet</h5> <h5 class="modal-title" id="electrumimportLabel">Import Wallet from file</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close"> <button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </button>
@@ -19,14 +19,15 @@
<div class="modal-body"> <div class="modal-body">
<p>You may import your air-gapped hardware wallet (such as ColdCard, Cobo Vault) by exporting a file and uploading it here.</p> <p>You may import your air-gapped hardware wallet (such as ColdCard, Cobo Vault) by exporting a file and uploading it here.</p>
<ul > <ul >
<li>Cobo Vault - <kbd>∙∙∙->Create Watch-Only Wallet in Electrum->export via microSD</kbd></li> <li>Cobo Vault - <kbd>Settings->Watch-Only Wallet->Wasabi Wallet/BTCPay->Export Wallet</kbd></li>
<li>ColdCard - <kbd>Advanced->MicroSD Card->Electrum Wallet</kbd></li> <li>ColdCard - <kbd>Advanced->MicroSD Card->Electrum Wallet</kbd> or <kbd>Advanced->MicroSD Card->Wasabi Wallet</kbd></li>
<li>Electrum - <kbd>File->Save Copy</kbd></li> <li>Electrum - <kbd>File->Save Copy</kbd></li>
<li>Wasabi - <kbd>Tools->Wallet Manager->Open Wallets Folder</kbd></li>
</ul> </ul>
<div class="form-group"> <div class="form-group">
<label asp-for="ElectrumWalletFile"></label> <label asp-for="WalletFile"></label>
<input type="file" class="form-control-file" asp-for="ElectrumWalletFile" required> <input type="file" class="form-control-file" asp-for="WalletFile" required>
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">