mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-01-19 05:33:31 +01:00
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:
parent
32f2acee53
commit
c8f182f13f
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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; }
|
||||
|
@ -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>
|
||||
|
@ -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">×</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">
|
||||
|
Loading…
Reference in New Issue
Block a user