Rename Coldcard to Electrum/Airgapped hardware wallets

Also adds support to read the xpub from the file directly ( Cobo Vault support)
This commit is contained in:
Kukks 2020-05-25 15:32:06 +02:00
parent 32f2acee53
commit c8f182f13f
6 changed files with 48 additions and 40 deletions

View File

@ -2121,7 +2121,7 @@ namespace BTCPayServer.Tests
.IsType<ViewResult>(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<ViewResult>(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<ViewResult>(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<ViewResult>(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<DerivationSchemeSettings>().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<BTCPayNetwork>("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);

View File

@ -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);

View File

@ -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<string>().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<string>(), derivationSchemeParser, ref result))
{
return false;
}

View File

@ -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; }

View File

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

View File

@ -7,21 +7,26 @@
<partial name="AddDerivationSchemes_NBXWalletGenerate" model="@(new GenerateWalletRequest())"/>
}
<div class="modal fade" id="coldcardimport" tabindex="-1" role="dialog" aria-labelledby="coldcardimport" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal fade" id="electrumimport" tabindex="-1" role="dialog" aria-labelledby="electrumimport" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<form class="modal-content" form method="post" enctype="multipart/form-data">
<div class="modal-header">
<h5 class="modal-title" id="coldcardimportLabel">Import Coldcard Wallet</h5>
<h5 class="modal-title" id="electrumimportLabel">Import Electrum/Air-gapped Hardware Wallet</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>You may import your Coldcard wallet by exporting the public details from <kbd>Advanced->MicroSD Card->Electrum Wallet</kbd> 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 >
<li>Cobo Vault - <kbd>∙∙∙->Create Watch-Only Wallet in Electrum->export via microSD</kbd></li>
<li>ColdCard - <kbd>Advanced->MicroSD Card->Electrum Wallet</kbd></li>
<li>Electrum - <kbd>File->Save Copy</kbd></li>
</ul>
<div class="form-group">
<label asp-for="ColdcardPublicFile"></label>
<label asp-for="ElectrumWalletFile"></label>
<input type="file" class="form-control-file" asp-for="ColdcardPublicFile" required>
<input type="file" class="form-control-file" asp-for="ElectrumWalletFile" required>
</div>
</div>
<div class="modal-footer">