mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-01-18 21:32:27 +01:00
Merge pull request #1745 from dennisreimann/recovery-seed
Recovery seed
This commit is contained in:
commit
27d7d03d4c
@ -130,11 +130,16 @@ namespace BTCPayServer.Tests
|
||||
Driver.WaitForElement(By.CssSelector($"#ScriptPubKeyType option[value={format}]")).Click();
|
||||
Logs.Tester.LogInformation("Trying to click btn-generate");
|
||||
Driver.WaitForElement(By.Id("btn-generate")).ForceClick();
|
||||
// Seed backup page
|
||||
AssertHappyMessage();
|
||||
if (string.IsNullOrEmpty(seed))
|
||||
{
|
||||
seed = Driver.FindElements(By.ClassName("alert-success")).First().FindElement(By.TagName("code")).Text;
|
||||
seed = Driver.FindElements(By.Id("recovery-phrase")).First().GetAttribute("data-mnemonic");
|
||||
}
|
||||
// Confirm seed backup
|
||||
Driver.FindElement(By.Id("confirm")).Click();
|
||||
Driver.FindElement(By.Id("submit")).Click();
|
||||
|
||||
WalletId = new WalletId(StoreId, cryptoCode);
|
||||
return new Mnemonic(seed);
|
||||
}
|
||||
|
@ -544,7 +544,6 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal(receiveAddr, s.Driver.FindElement(By.Id("vue-address")).GetAttribute("value"));
|
||||
|
||||
//send money to addr and ensure it changed
|
||||
|
||||
var sess = await s.Server.ExplorerClient.CreateWebsocketNotificationSessionAsync();
|
||||
sess.ListenAllTrackedSource();
|
||||
var nextEvent = sess.NextEventAsync();
|
||||
@ -556,6 +555,7 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.FindElement(By.CssSelector("button[value=generate-new-address]")).Click();
|
||||
Assert.NotEqual(receiveAddr, s.Driver.FindElement(By.Id("vue-address")).GetAttribute("value"));
|
||||
receiveAddr = s.Driver.FindElement(By.Id("vue-address")).GetAttribute("value");
|
||||
|
||||
//change the wallet and ensure old address is not there and generating a new one does not result in the prev one
|
||||
s.GoToStore(storeId.storeId);
|
||||
s.GenerateWallet("BTC", "", true, false);
|
||||
@ -565,12 +565,10 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.FindElement(By.CssSelector("button[value=generate-new-address]")).Click();
|
||||
Assert.NotEqual(receiveAddr, s.Driver.FindElement(By.Id("vue-address")).GetAttribute("value"));
|
||||
|
||||
|
||||
var invoiceId = s.CreateInvoice(storeId.storeName);
|
||||
var invoice = await s.Server.PayTester.InvoiceRepository.GetInvoice(invoiceId);
|
||||
var address = invoice.EntityToDTO().Addresses["BTC"];
|
||||
|
||||
|
||||
//wallet should have been imported to bitcoin core wallet in watch only mode.
|
||||
var result = await s.Server.ExplorerNode.GetAddressInfoAsync(BitcoinAddress.Create(address, Network.RegTest));
|
||||
Assert.True(result.IsWatchOnly);
|
||||
@ -664,13 +662,15 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal(parsedBip21.Amount.ToString(false), s.Driver.FindElement(By.Id($"Outputs_0__Amount")).GetAttribute("value"));
|
||||
Assert.Equal(parsedBip21.Address.ToString(), s.Driver.FindElement(By.Id($"Outputs_0__DestinationAddress")).GetAttribute("value"));
|
||||
|
||||
|
||||
s.GoToWallet(new WalletId(storeId.storeId, "BTC"), WalletsNavPages.Settings);
|
||||
|
||||
s.Driver.FindElement(By.Id("SettingsMenu")).ForceClick();
|
||||
s.Driver.FindElement(By.CssSelector("button[value=view-seed]")).Click();
|
||||
s.AssertHappyMessage();
|
||||
Assert.Equal(mnemonic.ToString(), s.Driver.FindElements(By.ClassName("alert-success")).First().FindElement(By.TagName("code")).Text);
|
||||
|
||||
// Seed backup page
|
||||
var recoveryPhrase = s.Driver.FindElements(By.Id("recovery-phrase")).First().GetAttribute("data-mnemonic");
|
||||
Assert.Equal(mnemonic.ToString(), recoveryPhrase);
|
||||
Assert.Contains("The recovery phrase will also be stored on a server as a hot wallet.", s.Driver.PageSource);
|
||||
}
|
||||
}
|
||||
void SetTransactionOutput(SeleniumTester s, int index, BitcoinAddress dest, decimal amount, bool subtract = false)
|
||||
|
@ -7,6 +7,7 @@ using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@ -191,6 +192,13 @@ namespace BTCPayServer.Controllers
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[Route("recovery-seed-backup")]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public IActionResult RecoverySeedBackup(RecoverySeedBackupViewModel vm)
|
||||
{
|
||||
return View("RecoverySeedBackup", vm);
|
||||
}
|
||||
|
||||
public IActionResult Error()
|
||||
{
|
||||
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
|
||||
|
@ -305,8 +305,17 @@ namespace BTCPayServer.Controllers
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
Html = $"Your wallet has been generated. Please store your seed securely! <br/><code class=\"alert-link\">{response.Mnemonic}</code>"
|
||||
Html = $"<span class='text-centered'>Your wallet has been generated.</span>"
|
||||
});
|
||||
var vm = new RecoverySeedBackupViewModel()
|
||||
{
|
||||
CryptoCode = cryptoCode,
|
||||
Mnemonic = response.Mnemonic,
|
||||
Passphrase = response.Passphrase,
|
||||
IsStored = request.SavePrivateKeys,
|
||||
ReturnUrl = Url.Action(nameof(UpdateStore), new { storeId })
|
||||
};
|
||||
return this.RedirectToRecoverySeedBackup(vm);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -9,6 +9,7 @@ using BTCPayServer.Data;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.ModelBinders;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Models.WalletViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Security;
|
||||
@ -1144,11 +1145,14 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
else
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
var recoveryVm = new RecoverySeedBackupViewModel()
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
Html = $"Please store your seed securely! <br/><code class=\"alert-link\">{seed}</code>"
|
||||
});
|
||||
CryptoCode = walletId.CryptoCode,
|
||||
Mnemonic = seed,
|
||||
IsStored = true,
|
||||
ReturnUrl = Url.Action(nameof(WalletSettings), new { walletId })
|
||||
};
|
||||
return this.RedirectToRecoverySeedBackup(recoveryVm);
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(WalletSettings));
|
||||
|
@ -12,12 +12,14 @@ using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Bitcoin;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@ -429,5 +431,23 @@ namespace BTCPayServer
|
||||
{
|
||||
ctx.Items["BTCPAY.STORESDATA"] = storeData;
|
||||
}
|
||||
|
||||
public static IActionResult RedirectToRecoverySeedBackup(this Controller controller, RecoverySeedBackupViewModel vm)
|
||||
{
|
||||
var redirectVm = new PostRedirectViewModel()
|
||||
{
|
||||
AspController = "Home",
|
||||
AspAction = "RecoverySeedBackup",
|
||||
Parameters =
|
||||
{
|
||||
new KeyValuePair<string, string>("cryptoCode", vm.CryptoCode),
|
||||
new KeyValuePair<string, string>("mnemonic", vm.Mnemonic),
|
||||
new KeyValuePair<string, string>("passphrase", vm.Passphrase),
|
||||
new KeyValuePair<string, string>("isStored", vm.IsStored ? "true" : "false"),
|
||||
new KeyValuePair<string, string>("returnUrl", vm.ReturnUrl)
|
||||
}
|
||||
};
|
||||
return controller.View("PostRedirect", redirectVm);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,21 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Models.StoreViewModels
|
||||
{
|
||||
public class RecoverySeedBackupViewModel
|
||||
{
|
||||
public string CryptoCode { get; set; }
|
||||
public string Mnemonic { get; set; }
|
||||
public string Passphrase { get; set; }
|
||||
public bool IsStored { get; set; }
|
||||
public string ReturnUrl { get; set; }
|
||||
|
||||
public string[] Words
|
||||
{
|
||||
get => Mnemonic.Split(" ");
|
||||
}
|
||||
}
|
||||
}
|
75
BTCPayServer/Views/Home/RecoverySeedBackup.cshtml
Normal file
75
BTCPayServer/Views/Home/RecoverySeedBackup.cshtml
Normal file
@ -0,0 +1,75 @@
|
||||
@model RecoverySeedBackupViewModel
|
||||
@{
|
||||
Layout = "_LayoutSimple";
|
||||
ViewData["Title"] = "Your recovery phrase";
|
||||
}
|
||||
|
||||
<style>
|
||||
@@media (min-width: 476px) { ol#recovery-phrase {max-height:16em;} }
|
||||
@@media (min-width: 768px) { ol#recovery-phrase {max-height:12em;} }
|
||||
@@media (min-width: 1200px) { ol#recovery-phrase {max-height:8em;} }
|
||||
form#recovery-confirmation button { position: absolute; bottom:0; left:50%; width:200px; margin-left:-100px; }
|
||||
form#recovery-confirmation button:not([disabled]) { display: none; }
|
||||
form#recovery-confirmation input:checked ~ button[disabled] { display: none; }
|
||||
form#recovery-confirmation input:checked + button:not([disabled]) { display: inline-block; }
|
||||
</style>
|
||||
|
||||
<div class="row justify-content-md-center">
|
||||
<div class="col-lg-10 text-center">
|
||||
<partial name="_StatusMessage" />
|
||||
|
||||
<div class="d-flex flex-column align-items-center justify-content-center">
|
||||
<span class="fa fa-info-circle align-self-center p-3" style="font-size:4em;"></span>
|
||||
|
||||
<h1 class="text-center text-primary mb-5">Secure your recovery phrase</h1>
|
||||
</div>
|
||||
<div class="lead text-center">
|
||||
<p>
|
||||
<span class="d-sm-block">The words below are called your recovery phrase.</span>
|
||||
<span class="d-sm-block"><strong>Write them down on a piece of paper in the exact order.</strong></span>
|
||||
</p>
|
||||
</div>
|
||||
<ol id="recovery-phrase" data-mnemonic="@Model.Mnemonic" class="my-5 d-flex flex-column flex-wrap justify-content-center align-items-center text-left p-0">
|
||||
@foreach (var word in Model.Words)
|
||||
{
|
||||
<li class="ml-4 px-3 py-2 text-secondary" style="flex: 0 1;min-width:10em;">
|
||||
<span class="text-dark h5">@word</span>
|
||||
</li>
|
||||
}
|
||||
</ol>
|
||||
<div class="lead text-center" style="max-width:36em;margin-left:auto;margin-right:auto;">
|
||||
@if (Model.IsStored)
|
||||
{
|
||||
<p>
|
||||
The recovery phrase is a backup that allows you to restore your wallet in case of a server crash.
|
||||
If you lose it or write it down incorrectly, you may permanently lose access to your funds.
|
||||
Do not photograph it. Do not store it digitally.
|
||||
</p>
|
||||
<p>
|
||||
<strong>The recovery phrase will also be stored on a server as a hot wallet.</strong>
|
||||
</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p>
|
||||
The recovery phrase allows you to access and restore your wallet.
|
||||
If you lose it or write it down incorrectly, you will permanently lose access to your funds.
|
||||
Do not photograph the recovery phrase and do not store it digitally.
|
||||
</p>
|
||||
<p>
|
||||
<strong>The recovery phrase will permanently be erased from the server.</strong>
|
||||
</p>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(Model.Passphrase))
|
||||
{
|
||||
<p class="mt-3 mb-0">Please make sure to also write down your passphrase.</p>
|
||||
}
|
||||
</div>
|
||||
<form id="recovery-confirmation" action="@Model.ReturnUrl" class="d-flex align-items-start justify-content-center" style="margin-top:4rem;padding-bottom: 80px">
|
||||
<label class="form-check-label lead order-2" for="confirm">I have written down my recovery phrase and stored it in a secure location</label>
|
||||
<input type="checkbox" class="mt-2 mr-3 order-1" id="confirm">
|
||||
<button type="submit" class="btn btn-primary btn-lg px-5 order-3" id="submit">Done</button>
|
||||
<button type="submit" class="btn btn-primary btn-lg px-5 order-3" disabled>Done</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
@ -1,12 +1,25 @@
|
||||
@model PostRedirectViewModel
|
||||
@{
|
||||
Layout = null;
|
||||
|
||||
var routeData = Context.GetRouteData();
|
||||
var routeParams = new Dictionary<string, string>();
|
||||
if (routeData != null)
|
||||
{
|
||||
routeParams["walletId"] = routeData.Values["walletId"]?.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
<html>
|
||||
<head></head>
|
||||
<body>
|
||||
<form method="post" id="postform" asp-action="@Model.AspAction" asp-controller="@Model.AspController" asp-route-walletId="@this.Context.GetRouteValue("walletId").ToString()">
|
||||
<form
|
||||
method="post"
|
||||
id="postform"
|
||||
asp-action="@Model.AspAction"
|
||||
asp-controller="@Model.AspController"
|
||||
asp-all-route-data="@routeParams"
|
||||
>
|
||||
@foreach(var o in Model.Parameters) {
|
||||
<input type="hidden" name="@o.Key" value="@o.Value" />
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user