Skip to content

Commit

Permalink
Wallet: Implement recovery phrase confirmation page
Browse files Browse the repository at this point in the history
  • Loading branch information
dennisreimann committed Feb 13, 2025
1 parent c1dce9f commit 113eeca
Show file tree
Hide file tree
Showing 5 changed files with 207 additions and 0 deletions.
1 change: 1 addition & 0 deletions BTCPayApp.Core/BTCPayAppConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ namespace BTCPayApp.Core;
public class BTCPayAppConfig
{
public const string Key = "appconfig";
public bool RecoveryPhraseVerified { get; set; }
public string? Passcode { get; set; }
public string? CurrentStoreId { get; set; }
}
145 changes: 145 additions & 0 deletions BTCPayApp.UI/Pages/Wallet/SeedConfirmationPage.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
@attribute [Route(Routes.WalletSeedConfirm)]
@using BTCPayApp.Core
@using BTCPayApp.Core.Auth
@using BTCPayApp.Core.Contracts
@using BTCPayApp.Core.Data
@using BTCPayApp.Core.Wallet
@using BTCPayApp.UI.Components.Layout
@inherits Fluxor.Blazor.Web.Components.FluxorComponent
@inject ConfigProvider ConfigProvider
@inject OnChainWalletManager OnChainWalletManager

<PageTitle>Confirm your recovery phrase</PageTitle>

<SectionContent SectionId="_Layout.Top">
<Titlebar Back>
<h1>Confirm your recovery phrase</h1>
</Titlebar>
</SectionContent>

<section class="container">
<AuthorizeView Policy="@AppPolicies.CanModifySettings">
<Authorized>
<p class="text-center">
Match the word to the number to verify.
</p>
<ValidationEditContext @ref="_validationEditContext" Model="Model" OnValidSubmit="HandleValidSubmit" SuccessMessage="@_successMessage" ErrorMessage="@_errorMessage">
@if (Words is not null)
{
<div class="box my-4">
<ol class="ask mt-3 mb-5">
@for (var i = 0; i < _ask.Length; i++)
{
var num = _ask[i];
<li value="@num">
<div class="rounded-pill@(Model.Words.Count == i ? " current" : "")">
@(Model.Words.Count > i ? Model.Words[i] : "...")
</div>
</li>
}
</ol>
<div class="words">
@foreach (var word in Shuffled)
{
<button type="button" class="btn bg-white rounded-pill" @onclick="() => AddWord(word)">@word</button>
}
</div>
</div>
}
@if (!Model.IsVerified)
{
<button class="btn btn-primary w-100 rounded-pill" type="submit" disabled="@(Model.Words.Count != _ask.Length)">
<span>Verify recovery phrase</span>
</button>
}
</ValidationEditContext>
</Authorized>
<NotAuthorized>
<Alert Type="danger">Unauthorized.</Alert>
</NotAuthorized>
</AuthorizeView>
</section>

@code {
private string? _errorMessage;
private string? _successMessage;
private BTCPayAppConfig? _config;
private ValidationEditContext? _validationEditContext;
VerificationModel Model { get; set; } = new();

private WalletConfig? Wallet { get; set; }
private string[]? Words { get; set; }
private string[]? Shuffled { get; set; }

private readonly int[] _ask = [
Random.Shared.Next(1, 3),
Random.Shared.Next(3, 5),
Random.Shared.Next(5, 7),
Random.Shared.Next(7, 9),
Random.Shared.Next(9, 11),
Random.Shared.Next(11, 13)
];

protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();

_config = await ConfigProvider.Get<BTCPayAppConfig>(BTCPayAppConfig.Key) ?? new BTCPayAppConfig();

Wallet = await OnChainWalletManager.GetConfig();
Words = Wallet?.Mnemonic.Split(' ');
Shuffled = Words?.OrderBy(_ => Guid.NewGuid()).ToArray();
}

private async Task HandleValidSubmit()
{
_errorMessage = _successMessage = null;
Model.IsVerified = false;

if (Words is null)
{
_errorMessage = "Recovery phrase not available";
}
else if (Model.Words.Count != _ask.Length)
{
_errorMessage = "Please fill all words.";
}
else
{
for (var i = 0; i < _ask.Length; i++)
{
var num = _ask[i] - 1;
var word = Model.Words[i];
var expected = Words[num];
if (word != expected)
{
_errorMessage = "Please check the words.";
Model.Words = [];
return;
}
}

_successMessage = "Good job, these are correct!";
Model.IsVerified = true;

if (!_config!.RecoveryPhraseVerified)
{
_config!.RecoveryPhraseVerified = true;
await ConfigProvider.Set(BTCPayAppConfig.Key, _config, true);
}
}
}

private void AddWord(string word)
{
if (Model.Words.Count < _ask.Length)
Model.Words.Add(word);
}

private class VerificationModel
{
public List<string> Words { get; set; } = [];
public bool IsVerified { get; set; }
}
}

57 changes: 57 additions & 0 deletions BTCPayApp.UI/Pages/Wallet/SeedConfirmationPage.razor.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
.container {
max-width: 500px;
}

.ask {
display: grid;
grid-auto-flow: column;
grid-template-rows: repeat(3, 50%);
grid-template-columns: repeat(2, 50%);
gap: var(--btcpay-space-m);
margin-right: 2em;
padding: 0;
}

.ask li::marker {
color: var(--btcpay-body-text-muted);
font-weight: var(--btcpay-font-weight-normal);
}

.ask li {
margin-left: 2.5em;
font-weight: var(--btcpay-font-weight-semibold);
}

.ask li .rounded-pill {
display: block;
border: 1px dashed var(--btcpay-body-border-medium);
background: var(--btcpay-bg-tile);
margin-left: var(--btcpay-space-xs);
padding: .5rem 1.5rem;
}

.ask li .rounded-pill.current {
border-color: 1px dashed var(--btcpay-body-border-light);
}

.words {
display: flex;
flex-wrap: wrap;
gap: var(--btcpay-space-m);
align-items: center;
justify-content: space-evenly;
}

.words .btn {
flex: 1 1 calc(50% - var(--btcpay-space-m));
--btcpay-btn-color: var(--btcpay-black);
--btcpay-btn-hover-color: var(--btcpay-black);
--btcpay-btn-active-color: var(--btcpay-black);
--btcpay-btn-bg: var(--btcpay-white);
}

@media (min-width: 400px) {
.words .btn {
flex: 1 1 calc(33% - var(--btcpay-space-m));
}
}
3 changes: 3 additions & 0 deletions BTCPayApp.UI/Pages/Wallet/SeedPage.razor
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@
}
</ol>
</div>
<a href="@Routes.WalletSeedConfirm" class="btn btn-primary w-100 rounded-pill">
Confirm recovery phrase
</a>
}
</Authorized>
<NotAuthorized>
Expand Down
1 change: 1 addition & 0 deletions BTCPayApp.UI/Routes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public static class Routes
public const string NotificationSettings = "/settings/notifications";
public const string WalletSettings = "/settings/wallet";
public const string WalletSeed = "/settings/wallet/seed";
public const string WalletSeedConfirm = "/settings/wallet/seed-confirmation";
public const string WalletFunds = "/settings/wallet/funds";
public const string LightningSettings = "/settings/lightning";
public const string ChannelsPeers = "/settings/lightning/channels";
Expand Down

0 comments on commit 113eeca

Please sign in to comment.