Support electrum segwit xpub format

This commit is contained in:
nicolas.dorier
2017-12-06 18:08:21 +09:00
parent a52a1901c4
commit 24ce325e31
6 changed files with 143 additions and 104 deletions

View File

@@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework> <TargetFramework>netcoreapp2.0</TargetFramework>
<Version>1.0.0.37</Version> <Version>1.0.0.38</Version>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Compile Remove="Build\dockerfiles\**" /> <Compile Remove="Build\dockerfiles\**" />

View File

@@ -235,9 +235,9 @@ namespace BTCPayServer.Controllers
[BitpayAPIConstraint(false)] [BitpayAPIConstraint(false)]
public async Task<IActionResult> CreateInvoice(CreateInvoiceModel model) public async Task<IActionResult> CreateInvoice(CreateInvoiceModel model)
{ {
model.Stores = await GetStores(GetUserId(), model.StoreId);
if (!ModelState.IsValid) if (!ModelState.IsValid)
{ {
model.Stores = await GetStores(GetUserId(), model.StoreId);
return View(model); return View(model);
} }
var store = await _StoreRepository.FindStore(model.StoreId, GetUserId()); var store = await _StoreRepository.FindStore(model.StoreId, GetUserId());

View File

@@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.Rendering;
using NBitcoin; using NBitcoin;
using NBitcoin.DataEncoders;
using NBitpayClient; using NBitpayClient;
using NBXplorer.DerivationStrategy; using NBXplorer.DerivationStrategy;
using System; using System;
@@ -92,7 +93,7 @@ namespace BTCPayServer.Controllers
StoresViewModel result = new StoresViewModel(); StoresViewModel result = new StoresViewModel();
result.StatusMessage = StatusMessage; result.StatusMessage = StatusMessage;
var stores = await _Repo.GetStoresByUserId(GetUserId()); var stores = await _Repo.GetStoresByUserId(GetUserId());
var balances = stores.Select(async s => string.IsNullOrEmpty(s.DerivationStrategy) ? Money.Zero : await _Wallet.GetBalance(ParseDerivationStrategy(s.DerivationStrategy))).ToArray(); var balances = stores.Select(async s => string.IsNullOrEmpty(s.DerivationStrategy) ? Money.Zero : await _Wallet.GetBalance(ParseDerivationStrategy(s.DerivationStrategy, null))).ToArray();
for (int i = 0; i < stores.Length; i++) for (int i = 0; i < stores.Length; i++)
{ {
@@ -195,7 +196,7 @@ namespace BTCPayServer.Controllers
{ {
if (!string.IsNullOrEmpty(model.DerivationScheme)) if (!string.IsNullOrEmpty(model.DerivationScheme))
{ {
var strategy = ParseDerivationStrategy(model.DerivationScheme); var strategy = ParseDerivationStrategy(model.DerivationScheme, model.DerivationSchemeFormat);
await _Wallet.TrackAsync(strategy); await _Wallet.TrackAsync(strategy);
await _CallbackController.RegisterCallbackUriAsync(strategy, Request); await _CallbackController.RegisterCallbackUriAsync(strategy, Request);
} }
@@ -230,21 +231,62 @@ namespace BTCPayServer.Controllers
} }
else else
{ {
var facto = new DerivationStrategyFactory(_Network); if (!string.IsNullOrEmpty(model.DerivationScheme))
var scheme = facto.Parse(model.DerivationScheme);
var line = scheme.GetLineFor(DerivationFeature.Deposit);
for (int i = 0; i < 10; i++)
{ {
var address = line.Derive((uint)i); try
model.AddressSamples.Add((line.Path.Derive((uint)i).ToString(), address.ScriptPubKey.GetDestinationAddress(_Network).ToString())); {
var scheme = ParseDerivationStrategy(model.DerivationScheme, model.DerivationSchemeFormat);
var line = scheme.GetLineFor(DerivationFeature.Deposit);
for (int i = 0; i < 10; i++)
{
var address = line.Derive((uint)i);
model.AddressSamples.Add((line.Path.Derive((uint)i).ToString(), address.ScriptPubKey.GetDestinationAddress(_Network).ToString()));
}
}
catch
{
ModelState.AddModelError(nameof(model.DerivationScheme), "Invalid Derivation Scheme");
}
} }
return View(model); return View(model);
} }
} }
private DerivationStrategyBase ParseDerivationStrategy(string derivationScheme) private DerivationStrategyBase ParseDerivationStrategy(string derivationScheme, string format)
{ {
if (format == "Electrum")
{
//Unsupported Electrum
//var p2wsh_p2sh = 0x295b43fU;
//var p2wsh = 0x2aa7ed3U;
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, new string[] { });
var data = Encoders.Base58Check.DecodeData(derivationScheme);
if (data.Length < 4)
throw new FormatException("data.Length < 4");
var prefix = Utils.ToUInt32(data, false);
if (!electrumMapping.TryGetValue(prefix, out string[] labels))
throw new FormatException("!electrumMapping.TryGetValue(prefix, out string[] labels)");
var standardPrefix = Utils.ToBytes(standard, false);
for (int i = 0; i < 4; i++)
data[i] = standardPrefix[i];
derivationScheme = new BitcoinExtPubKey(Encoders.Base58Check.EncodeData(data), Network.Main).ToNetwork(_Network).ToString();
foreach (var label in labels)
{
derivationScheme = derivationScheme + $"-[{label}]";
}
}
return new DerivationStrategyFactory(_Network).Parse(derivationScheme); return new DerivationStrategyFactory(_Network).Parse(derivationScheme);
} }

View File

@@ -1,5 +1,6 @@
using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Invoices;
using BTCPayServer.Validations; using BTCPayServer.Validations;
using Microsoft.AspNetCore.Mvc.Rendering;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
@@ -10,6 +11,21 @@ namespace BTCPayServer.Models.StoreViewModels
{ {
public class StoreViewModel public class StoreViewModel
{ {
class Format
{
public string Name { get; set; }
public string Value { get; set; }
}
public StoreViewModel()
{
var btcPay = new Format { Name = "BTCPay", Value = "BTCPay" };
DerivationSchemeFormat = btcPay.Value;
DerivationSchemeFormats = new SelectList(new Format[]
{
btcPay,
new Format { Name = "Electrum", Value = "Electrum" },
}, nameof(btcPay.Value), nameof(btcPay.Name), btcPay);
}
public string Id { get; set; } public string Id { get; set; }
[Display(Name = "Store Name")] [Display(Name = "Store Name")]
[Required] [Required]
@@ -29,12 +45,20 @@ namespace BTCPayServer.Models.StoreViewModels
set; set;
} }
[DerivationStrategyValidator]
public string DerivationScheme public string DerivationScheme
{ {
get; set; get; set;
} }
[Display(Name = "Derivation Scheme format")]
public string DerivationSchemeFormat
{
get;
set;
}
public SelectList DerivationSchemeFormats { get; set; }
[Display(Name = "Payment invalid if transactions fails to confirm after ... minutes")] [Display(Name = "Payment invalid if transactions fails to confirm after ... minutes")]
[Range(10, 60 * 24 * 31)] [Range(10, 60 * 24 * 31)]
public int MonitoringExpiration public int MonitoringExpiration

View File

@@ -1,32 +0,0 @@
using NBitcoin;
using NBXplorer.DerivationStrategy;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Text;
namespace BTCPayServer.Validations
{
public class DerivationStrategyValidatorAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (value == null)
{
return ValidationResult.Success;
}
var network = (Network)validationContext.GetService(typeof(Network));
if (network == null)
return new ValidationResult("No Network specified");
try
{
new DerivationStrategyFactory(network).Parse((string)value);
return ValidationResult.Success;
}
catch (Exception ex)
{
return new ValidationResult(ex.Message);
}
}
}
}

View File

@@ -56,72 +56,77 @@
<div class="form-group"> <div class="form-group">
<h5>Derivation Scheme</h5> <h5>Derivation Scheme</h5>
@if(Model.AddressSamples.Count == 0) @if(Model.AddressSamples.Count == 0)
{ {
<span>The DerivationScheme represents the destination of the funds received by your invoice. It is generated by your wallet software. Please, verify that you are generating the right addresses by clicking on 'Check ExtPubKey'</span> <span>The DerivationScheme represents the destination of the funds received by your invoice. It is generated by your wallet software. Please, verify that you are generating the right addresses by clicking on 'Check ExtPubKey'</span>
} }
</div> </div>
<div class="form-group"> <div class="form-group">
<input asp-for="DerivationScheme" class="form-control" /> <input asp-for="DerivationScheme" class="form-control" />
<span asp-validation-for="DerivationScheme" class="text-danger"></span> <span asp-validation-for="DerivationScheme" class="text-danger"></span>
</div> </div>
<div class="form-group">
<label asp-for="DerivationSchemeFormat"></label>
<select asp-for="DerivationSchemeFormat" asp-items="Model.DerivationSchemeFormats" class="form-control"></select>
</div>
<div class="form-group"> <div class="form-group">
@if(Model.AddressSamples.Count == 0) @if(Model.AddressSamples.Count == 0)
{ {
<table class="table"> <span>BTCPay format memo</span>
<thead class="thead-inverse"> <table class="table">
<tr> <thead class="thead-inverse">
<th>Address type</th> <tr>
<th>Example</th> <th>Address type</th>
</tr> <th>Example</th>
</thead> </tr>
<tbody> </thead>
<tr> <tbody>
<td>P2WPKH</td> <tr>
<td>xpub</td> <td>P2WPKH</td>
</tr> <td>xpub</td>
<tr> </tr>
<td>P2SH-P2WPKH</td> <tr>
<td>xpub-[p2sh]</td> <td>P2SH-P2WPKH</td>
</tr> <td>xpub-[p2sh]</td>
<tr> </tr>
<td>P2PKH</td> <tr>
<td>xpub-[legacy]</td> <td>P2PKH</td>
</tr> <td>xpub-[legacy]</td>
<tr> </tr>
<td>Multi-sig P2WSH</td> <tr>
<td>2-of-xpub1-xpub2</td> <td>Multi-sig P2WSH</td>
</tr> <td>2-of-xpub1-xpub2</td>
<tr> </tr>
<td>Multi-sig P2SH-P2WSH</td> <tr>
<td>2-of-xpub1-xpub2-[p2sh]</td> <td>Multi-sig P2SH-P2WSH</td>
</tr> <td>2-of-xpub1-xpub2-[p2sh]</td>
<tr> </tr>
<td>Multi-sig P2SH</td> <tr>
<td>2-of-xpub1-xpub2-[legacy]</td> <td>Multi-sig P2SH</td>
</tr> <td>2-of-xpub1-xpub2-[legacy]</td>
</tbody> </tr>
</table> </tbody>
} </table>
else }
{ else
<table class="table"> {
<thead class="thead-inverse"> <table class="table">
<tr> <thead class="thead-inverse">
<th>Key path</th> <tr>
<th>Address</th> <th>Key path</th>
</tr> <th>Address</th>
</thead> </tr>
<tbody> </thead>
@foreach(var sample in Model.AddressSamples) <tbody>
{ @foreach(var sample in Model.AddressSamples)
<tr> {
<td>@sample.KeyPath</td> <tr>
<td>@sample.Address</td> <td>@sample.KeyPath</td>
</tr> <td>@sample.Address</td>
} </tr>
</tbody> }
</table> </tbody>
} </table>
}
</div> </div>
<button name="command" type="submit" class="btn btn-success" value="Save">Save</button> <button name="command" type="submit" class="btn btn-success" value="Save">Save</button>
<button name="command" type="submit" class="btn btn-default" value="Check">Check ExtPubKey</button> <button name="command" type="submit" class="btn btn-default" value="Check">Check ExtPubKey</button>