Add parsing of cold card wallet

This commit is contained in:
nicolas.dorier 2019-05-09 00:40:30 +09:00
parent e5704abfb3
commit f2da64adad
No known key found for this signature in database
GPG key ID: 6618763EF09186FE
3 changed files with 153 additions and 28 deletions

View file

@ -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]
[Trait("Fast", "Fast")]
public void CanParseFilter()
@ -2574,6 +2582,20 @@ donation:
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]
[Trait("Fast", "Fast")]
public void CheckParseStatusMessageModel()

View file

@ -14,12 +14,51 @@ namespace BTCPayServer
{
public Network Network { 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)
{
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)
{
if (str == null)
@ -41,7 +80,7 @@ namespace BTCPayServer
}
}
if(!Network.Consensus.SupportSegwit)
if (!Network.Consensus.SupportSegwit)
hintedLabels.Add("legacy");
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('-');
bool hasLabel = false;
for (int i = 0; i < parts.Length; i++)
@ -84,11 +114,12 @@ namespace BTCPayServer
if (data.Length < 4)
continue;
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++)
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);
if (labels != null)
{
@ -136,7 +167,7 @@ namespace BTCPayServer
resultNoLabels = string.Join('-', resultNoLabels.Split('-').Where(p => !IsLabel(p)));
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)
return hinted;
}
@ -149,20 +180,20 @@ namespace BTCPayServer
}
/// <summary>
/// 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:
/// http://stackoverflow.com/questions/7802822/all-possible-combinations-of-a-list-of-values
/// </summary>
/// <typeparam name="T">type of the items on the input list</typeparam>
/// <param name="inputList">list of items</param>
/// <param name="minimumItems">minimum number of items wanted in the generated combinations,
/// if zero the empty combination is included,
/// default is one</param>
/// <param name="maximumItems">maximum number of items wanted in the generated combinations,
/// default is no maximum limit</param>
/// <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,
int maximumItems = int.MaxValue)
/// 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:
/// http://stackoverflow.com/questions/7802822/all-possible-combinations-of-a-list-of-values
/// </summary>
/// <typeparam name="T">type of the items on the input list</typeparam>
/// <param name="inputList">list of items</param>
/// <param name="minimumItems">minimum number of items wanted in the generated combinations,
/// if zero the empty combination is included,
/// default is one</param>
/// <param name="maximumItems">maximum number of items wanted in the generated combinations,
/// default is no maximum limit</param>
/// <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,
int maximumItems = int.MaxValue)
{
int nonEmptyCombinations = (int)Math.Pow(2, inputList.Count) - 1;
List<List<T>> listOfLists = new List<List<T>>(nonEmptyCombinations + 1);

View file

@ -21,7 +21,79 @@ namespace BTCPayServer
var result = new NBXplorer.DerivationStrategy.DerivationStrategyFactory(network.NBitcoinNetwork).Parse(derivationStrategy);
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()
{