mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-18 22:44:29 +01:00
Add parsing of cold card wallet
This commit is contained in:
@@ -883,6 +883,14 @@ namespace BTCPayServer.Tests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Fast", "Fast")]
|
||||||
|
public void CanParseColdcard()
|
||||||
|
{
|
||||||
|
var mnemonic = 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");
|
||||||
|
var coldcardWallet = "{\"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}";
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
[Trait("Fast", "Fast")]
|
[Trait("Fast", "Fast")]
|
||||||
public void CanParseFilter()
|
public void CanParseFilter()
|
||||||
@@ -2574,6 +2582,20 @@ donation:
|
|||||||
Assert.Throws<InvalidOperationException>(() => fetch.GetRatesAsync(default).GetAwaiter().GetResult());
|
Assert.Throws<InvalidOperationException>(() => fetch.GetRatesAsync(default).GetAwaiter().GetResult());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Fast", "Fast")]
|
||||||
|
public void ParseDerivationSchemeSettings()
|
||||||
|
{
|
||||||
|
var mainnet = new BTCPayNetworkProvider(NetworkType.Mainnet).GetNetwork("BTC");
|
||||||
|
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("{\"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.RootFingerprint);
|
||||||
|
Assert.Equal("Coldcard Import 0x60d1af8b", settings.Label);
|
||||||
|
Assert.Equal("49'/0'/0'", settings.AccountKeyPath.ToString());
|
||||||
|
Assert.Equal("ypub6WWc2gWwHbdnAAyJDnR4SPL1phRh7REqrPBfZeizaQ1EmTshieRXJC3Z5YoU4wkcdKHEjQGkh6AYEzCQC1Kz3DNaWSwdc1pc8416hAjzqyD", settings.AccountOriginal);
|
||||||
|
Assert.Equal(root.Derive(new KeyPath("m/49'/0'/0'")).Neuter().PubKey.WitHash.ScriptPubKey.Hash.ScriptPubKey, settings.AccountDerivation.Derive(new KeyPath()).ScriptPubKey);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
[Trait("Fast", "Fast")]
|
[Trait("Fast", "Fast")]
|
||||||
public void CheckParseStatusMessageModel()
|
public void CheckParseStatusMessageModel()
|
||||||
|
|||||||
@@ -14,12 +14,51 @@ namespace BTCPayServer
|
|||||||
{
|
{
|
||||||
public Network Network { get; set; }
|
public Network Network { get; set; }
|
||||||
public Script HintScriptPubKey { get; set; }
|
public Script HintScriptPubKey { get; set; }
|
||||||
|
static Dictionary<uint, string[]> electrumMapping;
|
||||||
|
|
||||||
|
static DerivationSchemeParser()
|
||||||
|
{
|
||||||
|
//Source https://github.com/spesmilo/electrum/blob/11733d6bc271646a00b69ff07657119598874da4/electrum/constants.py
|
||||||
|
electrumMapping = new Dictionary<uint, string[]>();
|
||||||
|
electrumMapping.Add(0x0488b21eU, new[] { "legacy" });
|
||||||
|
electrumMapping.Add(0x049d7cb2U, new string[] { "p2sh" });
|
||||||
|
electrumMapping.Add(0x4b24746U, Array.Empty<string>());
|
||||||
|
}
|
||||||
public DerivationSchemeParser(Network expectedNetwork)
|
public DerivationSchemeParser(Network expectedNetwork)
|
||||||
{
|
{
|
||||||
Network = expectedNetwork;
|
Network = expectedNetwork;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public DerivationStrategyBase ParseElectrum(string str)
|
||||||
|
{
|
||||||
|
if (str == null)
|
||||||
|
throw new ArgumentNullException(nameof(str));
|
||||||
|
str = str.Trim();
|
||||||
|
var data = Network.GetBase58CheckEncoder().DecodeData(str);
|
||||||
|
if (data.Length < 4)
|
||||||
|
throw new FormatException();
|
||||||
|
var prefix = Utils.ToUInt32(data, false);
|
||||||
|
|
||||||
|
var standardPrefix = Utils.ToBytes(0x0488b21eU, false);
|
||||||
|
for (int ii = 0; ii < 4; ii++)
|
||||||
|
data[ii] = standardPrefix[ii];
|
||||||
|
var extPubKey = new BitcoinExtPubKey(Network.GetBase58CheckEncoder().EncodeData(data), Network.Main).ToNetwork(Network);
|
||||||
|
if (!electrumMapping.TryGetValue(prefix, out string[] labels))
|
||||||
|
{
|
||||||
|
throw new FormatException();
|
||||||
|
}
|
||||||
|
if (labels.Length == 0)
|
||||||
|
return new DirectDerivationStrategy(extPubKey) { Segwit = true };
|
||||||
|
if (labels[0] == "legacy")
|
||||||
|
return new DirectDerivationStrategy(extPubKey) { Segwit = false };
|
||||||
|
if (labels[0] == "p2sh")
|
||||||
|
return new DerivationStrategyFactory(Network).Parse(extPubKey.ToString() + "-[p2sh]");
|
||||||
|
throw new FormatException();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public DerivationStrategyBase Parse(string str)
|
public DerivationStrategyBase Parse(string str)
|
||||||
{
|
{
|
||||||
if (str == null)
|
if (str == null)
|
||||||
@@ -41,7 +80,7 @@ namespace BTCPayServer
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!Network.Consensus.SupportSegwit)
|
if (!Network.Consensus.SupportSegwit)
|
||||||
hintedLabels.Add("legacy");
|
hintedLabels.Add("legacy");
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -53,15 +92,6 @@ namespace BTCPayServer
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
Dictionary<uint, string[]> electrumMapping = new Dictionary<uint, string[]>();
|
|
||||||
//Source https://github.com/spesmilo/electrum/blob/9edffd17542de5773e7284a8c8a2673c766bb3c3/lib/bitcoin.py
|
|
||||||
var standard = 0x0488b21eU;
|
|
||||||
electrumMapping.Add(standard, new[] { "legacy" });
|
|
||||||
var p2wpkh_p2sh = 0x049d7cb2U;
|
|
||||||
electrumMapping.Add(p2wpkh_p2sh, new string[] { "p2sh" });
|
|
||||||
var p2wpkh = 0x4b24746U;
|
|
||||||
electrumMapping.Add(p2wpkh, Array.Empty<string>());
|
|
||||||
|
|
||||||
var parts = str.Split('-');
|
var parts = str.Split('-');
|
||||||
bool hasLabel = false;
|
bool hasLabel = false;
|
||||||
for (int i = 0; i < parts.Length; i++)
|
for (int i = 0; i < parts.Length; i++)
|
||||||
@@ -84,11 +114,12 @@ namespace BTCPayServer
|
|||||||
if (data.Length < 4)
|
if (data.Length < 4)
|
||||||
continue;
|
continue;
|
||||||
var prefix = Utils.ToUInt32(data, false);
|
var prefix = Utils.ToUInt32(data, false);
|
||||||
var standardPrefix = Utils.ToBytes(Network.NetworkType == NetworkType.Mainnet ? 0x0488b21eU : 0x043587cf, false);
|
|
||||||
|
var standardPrefix = Utils.ToBytes(0x0488b21eU, false);
|
||||||
for (int ii = 0; ii < 4; ii++)
|
for (int ii = 0; ii < 4; ii++)
|
||||||
data[ii] = standardPrefix[ii];
|
data[ii] = standardPrefix[ii];
|
||||||
|
var derivationScheme = new BitcoinExtPubKey(Network.GetBase58CheckEncoder().EncodeData(data), Network.Main).ToNetwork(Network).ToString();
|
||||||
|
|
||||||
var derivationScheme = new BitcoinExtPubKey(Network.GetBase58CheckEncoder().EncodeData(data), Network).ToString();
|
|
||||||
electrumMapping.TryGetValue(prefix, out string[] labels);
|
electrumMapping.TryGetValue(prefix, out string[] labels);
|
||||||
if (labels != null)
|
if (labels != null)
|
||||||
{
|
{
|
||||||
@@ -136,7 +167,7 @@ namespace BTCPayServer
|
|||||||
resultNoLabels = string.Join('-', resultNoLabels.Split('-').Where(p => !IsLabel(p)));
|
resultNoLabels = string.Join('-', resultNoLabels.Split('-').Where(p => !IsLabel(p)));
|
||||||
foreach (var labels in ItemCombinations(hintLabels.ToList()))
|
foreach (var labels in ItemCombinations(hintLabels.ToList()))
|
||||||
{
|
{
|
||||||
var hinted = facto.Parse(resultNoLabels + '-' + string.Join('-', labels.Select(l=>$"[{l}]").ToArray()));
|
var hinted = facto.Parse(resultNoLabels + '-' + string.Join('-', labels.Select(l => $"[{l}]").ToArray()));
|
||||||
if (HintScriptPubKey == hinted.Derive(firstKeyPath).ScriptPubKey)
|
if (HintScriptPubKey == hinted.Derive(firstKeyPath).ScriptPubKey)
|
||||||
return hinted;
|
return hinted;
|
||||||
}
|
}
|
||||||
@@ -149,20 +180,20 @@ namespace BTCPayServer
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Method to create lists containing possible combinations of an input list of items. This is
|
/// Method to create lists containing possible combinations of an input list of items. This is
|
||||||
/// basically copied from code by user "jaolho" on this thread:
|
/// basically copied from code by user "jaolho" on this thread:
|
||||||
/// http://stackoverflow.com/questions/7802822/all-possible-combinations-of-a-list-of-values
|
/// http://stackoverflow.com/questions/7802822/all-possible-combinations-of-a-list-of-values
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="T">type of the items on the input list</typeparam>
|
/// <typeparam name="T">type of the items on the input list</typeparam>
|
||||||
/// <param name="inputList">list of items</param>
|
/// <param name="inputList">list of items</param>
|
||||||
/// <param name="minimumItems">minimum number of items wanted in the generated combinations,
|
/// <param name="minimumItems">minimum number of items wanted in the generated combinations,
|
||||||
/// if zero the empty combination is included,
|
/// if zero the empty combination is included,
|
||||||
/// default is one</param>
|
/// default is one</param>
|
||||||
/// <param name="maximumItems">maximum number of items wanted in the generated combinations,
|
/// <param name="maximumItems">maximum number of items wanted in the generated combinations,
|
||||||
/// default is no maximum limit</param>
|
/// default is no maximum limit</param>
|
||||||
/// <returns>list of lists for possible combinations of the input items</returns>
|
/// <returns>list of lists for possible combinations of the input items</returns>
|
||||||
public static List<List<T>> ItemCombinations<T>(List<T> inputList, int minimumItems = 1,
|
public static List<List<T>> ItemCombinations<T>(List<T> inputList, int minimumItems = 1,
|
||||||
int maximumItems = int.MaxValue)
|
int maximumItems = int.MaxValue)
|
||||||
{
|
{
|
||||||
int nonEmptyCombinations = (int)Math.Pow(2, inputList.Count) - 1;
|
int nonEmptyCombinations = (int)Math.Pow(2, inputList.Count) - 1;
|
||||||
List<List<T>> listOfLists = new List<List<T>>(nonEmptyCombinations + 1);
|
List<List<T>> listOfLists = new List<List<T>>(nonEmptyCombinations + 1);
|
||||||
|
|||||||
@@ -21,7 +21,79 @@ namespace BTCPayServer
|
|||||||
var result = new NBXplorer.DerivationStrategy.DerivationStrategyFactory(network.NBitcoinNetwork).Parse(derivationStrategy);
|
var result = new NBXplorer.DerivationStrategy.DerivationStrategyFactory(network.NBitcoinNetwork).Parse(derivationStrategy);
|
||||||
return new DerivationSchemeSettings(result, network) { AccountOriginal = derivationStrategy.Trim() };
|
return new DerivationSchemeSettings(result, network) { AccountOriginal = derivationStrategy.Trim() };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static bool TryParseFromColdcard(string coldcardExport, BTCPayNetwork network, out DerivationSchemeSettings settings)
|
||||||
|
{
|
||||||
|
settings = null;
|
||||||
|
if (coldcardExport == null)
|
||||||
|
throw new ArgumentNullException(nameof(coldcardExport));
|
||||||
|
if (network == null)
|
||||||
|
throw new ArgumentNullException(nameof(network));
|
||||||
|
var result = new DerivationSchemeSettings();
|
||||||
|
var derivationSchemeParser = new DerivationSchemeParser(network.NBitcoinNetwork);
|
||||||
|
JObject jobj = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
jobj = JObject.Parse(coldcardExport);
|
||||||
|
jobj = (JObject)jobj["keystore"];
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jobj.ContainsKey("xpub"))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
result.AccountOriginal = jobj["xpub"].Value<string>().Trim();
|
||||||
|
result.AccountDerivation = derivationSchemeParser.ParseElectrum(result.AccountOriginal);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jobj.ContainsKey("label"))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
result.Label = jobj["label"].Value<string>();
|
||||||
|
}
|
||||||
|
catch { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jobj.ContainsKey("ckcc_xfp"))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
result.RootFingerprint = new HDFingerprint(jobj["ckcc_xfp"].Value<uint>());
|
||||||
|
}
|
||||||
|
catch { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jobj.ContainsKey("derivation"))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
result.AccountKeyPath = new KeyPath(jobj["derivation"].Value<string>());
|
||||||
|
}
|
||||||
|
catch { return false; }
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result.AccountKeyPath = new KeyPath();
|
||||||
|
}
|
||||||
|
settings = result;
|
||||||
|
settings.Network = network;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
public DerivationSchemeSettings()
|
public DerivationSchemeSettings()
|
||||||
{
|
{
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user