Merge pull request #1212 from Kukks/generatewallet

Generate wallet
This commit is contained in:
Nicolas Dorier 2019-12-18 16:35:34 +09:00 committed by GitHub
commit ed6a01469a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 183 additions and 16 deletions

View File

@ -109,6 +109,25 @@ namespace BTCPayServer.Tests
return (usr, Driver.FindElement(By.Id("Id")).GetAttribute("value"));
}
public string GenerateWallet(string cryptoCode = "BTC", string seed = "", bool importkeys = false, bool privkeys = false)
{
Driver.FindElement(By.Id($"Modify{cryptoCode}")).ForceClick();
Driver.FindElement(By.Id("import-from-btn")).ForceClick();
Driver.FindElement(By.Id("nbxplorergeneratewalletbtn")).ForceClick();
Driver.FindElement(By.Id("ExistingMnemonic")).SendKeys(seed);
SetCheckbox(Driver.FindElement(By.Id("SavePrivateKeys")), privkeys);
SetCheckbox(Driver.FindElement(By.Id("ImportKeysToRPC")), importkeys);
Driver.FindElement(By.Id("btn-generate")).ForceClick();
AssertHappyMessage();
if (string.IsNullOrEmpty(seed))
{
seed = Driver.FindElements(By.ClassName("alert-success")).First().FindElement(By.TagName("code")).Text;
}
Driver.FindElement(By.Id("Confirm")).ForceClick();
AssertHappyMessage();
return seed;
}
public void AddDerivationScheme(string cryptoCode = "BTC", string derivationScheme = "xpub661MyMwAqRbcGABgHMUXDzPzH1tU7eZaAaJQXhDXsSxsqyQzQeU6kznNfSuAyqAK9UaWSaZaMFdNiY5BCF4zBPAzSnwfUAwUhwttuAKwfRX-[legacy]")
{
Driver.FindElement(By.Id($"Modify{cryptoCode}")).ForceClick();
@ -219,6 +238,11 @@ namespace BTCPayServer.Tests
{
element.Click();
}
if (value != element.Selected)
{
SetCheckbox(element, value);
}
}
public void SetCheckbox(SeleniumTester s, string inputName, bool value)

View File

@ -408,14 +408,28 @@ namespace BTCPayServer.Tests
{
await s.StartAsync();
s.RegisterNewUser(true);
s.CreateNewStore();
var storeId = s.CreateNewStore();
// In this test, we try to spend from a manual seed. We import the xpub 49'/0'/0', then try to use the seed
// to sign the transaction
var 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 mnemonic = s.GenerateWallet("BTC", "", true, false);
var invoiceId = s.CreateInvoice(storeId.storeId);
var invoice = await s.Server.PayTester.InvoiceRepository.GetInvoice( invoiceId);
var address = invoice.EntityToDTO().Addresses["BTC"];
var result = await s.Server.ExplorerNode.GetAddressInfoAsync(BitcoinAddress.Create(address, Network.RegTest));
Assert.True(result.IsWatchOnly);
s.GoToStore(storeId.storeId);
mnemonic = s.GenerateWallet("BTC", "", true, true);
var root = new Mnemonic(mnemonic).DeriveExtKey();
s.AddDerivationScheme("BTC", "ypub6WWc2gWwHbdnAAyJDnR4SPL1phRh7REqrPBfZeizaQ1EmTshieRXJC3Z5YoU4wkcdKHEjQGkh6AYEzCQC1Kz3DNaWSwdc1pc8416hAjzqyD");
var tx = s.Server.ExplorerNode.SendToAddress(BitcoinAddress.Create("bcrt1qmxg8fgnmkp354vhe78j6sr4ut64tyz2xyejel4", Network.RegTest), Money.Coins(3.0m));
invoiceId = s.CreateInvoice(storeId.storeId);
invoice = await s.Server.PayTester.InvoiceRepository.GetInvoice( invoiceId);
address = invoice.EntityToDTO().Addresses["BTC"];
result = await s.Server.ExplorerNode.GetAddressInfoAsync(BitcoinAddress.Create(address, Network.RegTest));
Assert.False(result.IsWatchOnly);
var tx = s.Server.ExplorerNode.SendToAddress(BitcoinAddress.Create(address, Network.RegTest), Money.Coins(3.0m));
s.Server.ExplorerNode.Generate(1);
s.Driver.FindElement(By.Id("Wallets")).Click();
@ -429,8 +443,8 @@ namespace BTCPayServer.Tests
// We setup the fingerprint and the account key path
s.Driver.FindElement(By.Id("WalletSettings")).ForceClick();
s.Driver.FindElement(By.Id("AccountKeys_0__MasterFingerprint")).SendKeys("8bafd160");
s.Driver.FindElement(By.Id("AccountKeys_0__AccountKeyPath")).SendKeys("m/49'/0'/0'" + Keys.Enter);
// s.Driver.FindElement(By.Id("AccountKeys_0__MasterFingerprint")).SendKeys("8bafd160");
// s.Driver.FindElement(By.Id("AccountKeys_0__AccountKeyPath")).SendKeys("m/49'/0'/0'" + Keys.Enter);
// Check the tx sent earlier arrived
s.Driver.FindElement(By.Id("WalletTransactions")).ForceClick();
@ -471,7 +485,7 @@ namespace BTCPayServer.Tests
}
}
SignWith(mnemonic);
var accountKey = root.Derive(new KeyPath("m/49'/0'/0'")).GetWif(Network.RegTest).ToString();
var accountKey = root.Derive(new KeyPath("m/84'/1'/0'")).GetWif(Network.RegTest).ToString();
SignWith(accountKey);
}
}

View File

@ -17,6 +17,7 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using NBitcoin;
using NBXplorer.DerivationStrategy;
using NBXplorer.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
@ -131,6 +132,7 @@ namespace BTCPayServer.Controllers
vm.Config = derivation.ToJson();
}
vm.Enabled = !store.GetStoreBlob().IsExcluded(new PaymentMethodId(vm.CryptoCode, PaymentTypes.BTCLike));
vm.CanUseGenerateWallet = CanUseGenerateWallet();
}
private DerivationSchemeSettings GetExistingDerivationStrategy(string cryptoCode, StoreData store)
@ -178,7 +180,7 @@ namespace BTCPayServer.Controllers
Message = "Config file was not in the correct format"
});
vm.Confirmation = false;
return View(vm);
return View(nameof(AddDerivationScheme),vm);
}
}
@ -192,7 +194,7 @@ namespace BTCPayServer.Controllers
Message = "Coldcard public file was not in the correct format"
});
vm.Confirmation = false;
return View(vm);
return View(nameof(AddDerivationScheme),vm);
}
}
else
@ -228,7 +230,7 @@ namespace BTCPayServer.Controllers
{
ModelState.AddModelError(nameof(vm.DerivationScheme), "Invalid Derivation Scheme");
vm.Confirmation = false;
return View(vm);
return View(nameof(AddDerivationScheme),vm);
}
}
@ -319,6 +321,48 @@ namespace BTCPayServer.Controllers
return ShowAddresses(vm, strategy);
}
[HttpPost]
[Route("{storeId}/derivations/{cryptoCode}/generatenbxwallet")]
public async Task<IActionResult> GenerateNBXWallet(string storeId, string cryptoCode,
GenerateWalletRequest request)
{
if (!CanUseGenerateWallet())
{
return NotFound();
}
var network = _NetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
var client = _ExplorerProvider.GetExplorerClient(cryptoCode);
var response = await client.GenerateWalletAsync(request);
var store = HttpContext.GetStoreData();
var result = await AddDerivationScheme(storeId,
new DerivationSchemeViewModel()
{
Confirmation = false,
Network = network,
RootFingerprint = response.AccountKeyPath.MasterFingerprint.ToString(),
RootKeyPath = network.GetRootKeyPath(),
CryptoCode = cryptoCode,
DerivationScheme = response.DerivationScheme.ToString(),
Source = "NBXplorer",
AccountKey = response.AccountHDKey.Neuter().ToWif(),
DerivationSchemeFormat = "BTCPay",
KeyPath = response.AccountKeyPath.KeyPath.ToString(),
Enabled = !store.GetStoreBlob()
.IsExcluded(new PaymentMethodId(cryptoCode, PaymentTypes.BTCLike))
}, cryptoCode);
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Success,
Html = !string.IsNullOrEmpty(request.ExistingMnemonic)
? "Your wallet has been imported."
: $"Your wallet has been generated. Please store your seed securely! <br/><code>{response.Mnemonic}</code>"
});
return result;
}
private async Task<string> ReadAllText(IFormFile file)
{
using (var stream = new StreamReader(file.OpenReadStream()))
@ -345,7 +389,12 @@ namespace BTCPayServer.Controllers
}
vm.Confirmation = true;
ModelState.Remove(nameof(vm.Config)); // Remove the cached value
return View(vm);
return View(nameof(AddDerivationScheme),vm);
}
private bool CanUseGenerateWallet()
{
return (_BTCPayEnv.IsDevelopping || User.IsInRole(Roles.ServerAdmin) || _CssThemeManager.AllowGenerateWalletForAll);
}
}
}

View File

@ -88,8 +88,11 @@ namespace BTCPayServer.HostedServices
RootAppId = data.RootAppId;
DomainToAppMapping = data.DomainToAppMapping;
AllowLightningInternalNodeForAll = data.AllowLightningInternalNodeForAll;
AllowGenerateWalletForAll = data.AllowGenerateWalletForAll;
}
public bool AllowGenerateWalletForAll { get; set; }
public bool AllowLightningInternalNodeForAll { get; set; }
}

View File

@ -41,6 +41,7 @@ namespace BTCPayServer.Models.StoreViewModels
public string DerivationSchemeFormat { get; set; }
public string AccountKey { get; set; }
public BTCPayNetwork Network { get; set; }
public bool CanUseGenerateWallet { get; set; }
public RootedKeyPath GetAccountKeypath()
{

View File

@ -23,6 +23,8 @@ namespace BTCPayServer.Services
public bool DiscourageSearchEngines { get; set; }
[Display(Name = "Allow non-admins to use the internal lightning node in their stores")]
public bool AllowLightningInternalNodeForAll { get; set; }
[Display(Name = "Allow non-admins to use the NBXplorer wallet generator in their stores")]
public bool AllowGenerateWalletForAll { get; set; }
[Display(Name = "Display app on website root")]
public string RootAppId { get; set; }

View File

@ -32,6 +32,11 @@
<label asp-for="AllowLightningInternalNodeForAll" class="form-check-label"></label>
<span asp-validation-for="AllowLightningInternalNodeForAll" class="text-danger"></span>
</div>
<div class="form-check">
<input asp-for="AllowGenerateWalletForAll" type="checkbox" class="form-check-input"/>
<label asp-for="AllowGenerateWalletForAll" class="form-check-label"></label>
<span asp-validation-for="AllowGenerateWalletForAll" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="RootAppId"></label>

View File

@ -34,7 +34,7 @@
{
<template id="btcpayservervault_template">
<div class="modal-dialog" role="document">
<form class="modal-content" form method="post" enctype="multipart/form-data">
<form class="modal-content" method="post" enctype="multipart/form-data">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">Address verification</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
@ -55,7 +55,7 @@
</div>
</template>
}
<form method="post">
<form method="post" asp-action="AddDerivationScheme" asp-route-cryptoCode="@Model.CryptoCode" >
<input id="Config" asp-for="Config" type="hidden" />
@ -75,7 +75,7 @@
<div class="dropdown mt-2 text-right">
<div class="btn-group">
<button class="btn btn-link dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<button class="btn btn-link dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" id="import-from-btn">
Import from...
</button>
<div class="dropdown-menu dropdown-menu-right">
@ -85,6 +85,10 @@
{
<button class="dropdown-item check-for-vault" type="button">... the vault (preview)</button>
}
@if (Model.CanUseGenerateWallet)
{
<button class="dropdown-item" data-toggle="modal" data-target="#nbxplorergeneratewallet" type="button" id="nbxplorergeneratewalletbtn">... a new/existing seed.</button>
}
</div>
</div>
</div>

View File

@ -1,5 +1,9 @@
@using NBXplorer.Models
@model DerivationSchemeViewModel
@if (Model.CanUseGenerateWallet)
{
<partial name="AddDerivationSchemes_NBXWalletGenerate" model="@(new GenerateWalletRequest())"/>
}
<div class="modal fade" id="ledgerimport" tabindex="-1" role="dialog" aria-labelledby="ledgerimport" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content" form method="post">

View File

@ -0,0 +1,61 @@
@using NBitcoin
@model NBXplorer.Models.GenerateWalletRequest
<div class="modal fade" id="nbxplorergeneratewallet" tabindex="-1" role="dialog" aria-labelledby="nbxplorergeneratewallet" aria-hidden="true">
<div class="modal-dialog" role="document">
<form class="modal-content" form method="post" asp-action="GenerateNBXWallet" enctype="multipart/form-data">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">Generate a wallet with a seed</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>You may generate a wallet with a seed and import the xpub it into BTCPay. You can optionally also tell NBX to import the keys to the node wallet to be able to view & spend received funds from it.</p>
<div class="form-group">
<label asp-for="ExistingMnemonic">Existing Seed</label>
<input type="text" asp-for="ExistingMnemonic" class="form-control"/>
<span asp-validation-for="ExistingMnemonic" class="text-danger"></span>
<p class="text-black-50">You can choose to import an existing mnemonic seed phrase. If you leave blank, we will generate one for you.</p>
</div>
<div class="form-group">
<label asp-for="ScriptPubKeyType">Address type</label>
<select class="form-control" asp-for="ScriptPubKeyType">
<option value="@ScriptPubKeyType.Segwit">Segwit (Recommended, cheapest transaction fee)</option>
<option value="@ScriptPubKeyType.SegwitP2SH">Segwit wrapped (less cheap but compatible with old wallets)</option>
<option value="@ScriptPubKeyType.Legacy">Legacy (Not recommended)</option>
</select>
<span asp-validation-for="ScriptPubKeyType" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="AccountNumber">Account</label>
<select asp-for="AccountNumber" class="form-control">
@for (int i = 0; i < 20; i++)
{
<option value="@i">@i</option>
}
</select>
<span asp-validation-for="AccountNumber" class="text-danger"></span>
</div>
<div class="form-group">
<input type="checkbox" class="form-check-inline" asp-for="SavePrivateKeys"/>
<label asp-for="SavePrivateKeys">Is hot wallet</label>
<span asp-validation-for="SavePrivateKeys" class="text-danger"></span>
<p class="text-danger">If checked, each private key associated with an address generated will be stored as metadata in NBXplorer. While convenient, this means that anyone with access to your server will have access to your private keys and will be able to steal your funds.</p>
</div>
<div class="form-group">
<input type="checkbox" class="form-check-inline" asp-for="ImportKeysToRPC"/>
<label asp-for="ImportKeysToRPC">Import keys to RPC</label>
<span asp-validation-for="ImportKeysToRPC" class="text-danger"></span>
<p class="text-black-50">If checked, each address generated will be imported into the node wallet so that you can view your balance through your node. When this is enabled alongside <code>Is hot wallet</code>, you're also able to use the node wallet to spend (this works pretty well in conjunction with apps such as FullyNoded).</p>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button type="submit" class="btn btn-primary" id="btn-generate">Generate</button>
</div>
</form>
</div>
</div>