mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-01-19 13:43:46 +01:00
commit
ed6a01469a
@ -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)
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
{
|
||||
|
@ -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; }
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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">
|
||||
|
@ -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">×</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>
|
Loading…
Reference in New Issue
Block a user