mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-18 14:34:23 +01:00
Support electrum segwit xpub format
This commit is contained in:
@@ -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\**" />
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user