Refactor vault (#6678)

* Use Blazor for the Vault code

* Put elements in different file

* Controller abstraction

* Break into VaultElement
This commit is contained in:
Nicolas Dorier
2025-04-21 17:09:46 +09:00
committed by GitHub
parent ec603a3804
commit 2f26979ed7
46 changed files with 1367 additions and 1865 deletions

View File

@@ -100,6 +100,7 @@ namespace BTCPayServer.Security
var sha = GetSha256(script); var sha = GetSha256(script);
Add("script-src", $"'sha256-{sha}'"); Add("script-src", $"'sha256-{sha}'");
} }
static string GetSha256(string script) static string GetSha256(string script)
{ {
return Convert.ToBase64String(Hashes.SHA256(Encoding.UTF8.GetBytes(script.Replace("\r\n", "\n", StringComparison.Ordinal)))); return Convert.ToBase64String(Hashes.SHA256(Encoding.UTF8.GetBytes(script.Replace("\r\n", "\n", StringComparison.Ordinal))));

View File

@@ -16,6 +16,8 @@ namespace BTCPayServer
public static ScriptPubKeyType ScriptPubKeyType(this DerivationStrategyBase derivationStrategyBase) public static ScriptPubKeyType ScriptPubKeyType(this DerivationStrategyBase derivationStrategyBase)
{ {
if (derivationStrategyBase is TaprootDerivationStrategy)
return NBitcoin.ScriptPubKeyType.TaprootBIP86;
if (IsSegwitCore(derivationStrategyBase)) if (IsSegwitCore(derivationStrategyBase))
{ {
return NBitcoin.ScriptPubKeyType.Segwit; return NBitcoin.ScriptPubKeyType.Segwit;

View File

@@ -36,7 +36,7 @@ namespace BTCPayServer
["apdu"] = Encoders.Hex.EncodeData(apdu) ["apdu"] = Encoders.Hex.EncodeData(apdu)
}, cancellationToken); }, cancellationToken);
var data = Encoders.Hex.DecodeData(resp["data"].Value<string>()); var data = Encoders.Hex.DecodeData(resp["data"].Value<string>());
return new NtagResponse(data, resp["status"].Value<ushort>()); return new NtagResponse(data, resp["status"]!.Value<ushort>());
} }
} }
} }

View File

@@ -54,7 +54,7 @@
<PackageReference Include="NBitcoin" Version="8.0.8" /> <PackageReference Include="NBitcoin" Version="8.0.8" />
<PackageReference Include="YamlDotNet" Version="8.0.0" /> <PackageReference Include="YamlDotNet" Version="8.0.0" />
<PackageReference Include="BIP78.Sender" Version="0.2.4" /> <PackageReference Include="BIP78.Sender" Version="0.2.4" />
<PackageReference Include="BTCPayServer.Hwi" Version="2.0.2" /> <PackageReference Include="BTCPayServer.Hwi" Version="2.0.6" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.6.9" /> <PackageReference Include="BTCPayServer.Lightning.All" Version="1.6.9" />
<PackageReference Include="CsvHelper" Version="32.0.3" /> <PackageReference Include="CsvHelper" Version="32.0.3" />
<PackageReference Include="Dapper" Version="2.1.35" /> <PackageReference Include="Dapper" Version="2.1.35" />

View File

@@ -3,13 +3,15 @@
@inject IFileVersionProvider FileVersionProvider @inject IFileVersionProvider FileVersionProvider
@inject BTCPayServerOptions BTCPayServerOptions @inject BTCPayServerOptions BTCPayServerOptions
<svg role="img" class="icon icon-@Symbol"> <svg role="img" class="icon icon-@Symbol @(string.IsNullOrWhiteSpace(Class) ? "" : Class)">
<use href="@GetPathTo(Symbol)"></use> <use href="@GetPathTo(Symbol)"></use>
</svg> </svg>
@code { @code {
[Parameter, EditorRequired] [Parameter, EditorRequired]
public string Symbol { get; set; } public string Symbol { get; set; }
[Parameter]
public string Class { get; set; }
private string GetPathTo(string symbol) private string GetPathTo(string symbol)
{ {
var versioned = FileVersionProvider.AddFileVersionToPath(default, "img/icon-sprite.svg"); var versioned = FileVersionProvider.AddFileVersionToPath(default, "img/icon-sprite.svg");

View File

@@ -0,0 +1,64 @@
@inherits VaultElement
@using System.IO
@using Microsoft.AspNetCore.Mvc.Localization
@using Microsoft.AspNetCore.Razor.TagHelpers
@using Microsoft.Extensions.Localization
<div class="vault-feedback mb-2 d-flex align-items-center">
<Icon Class=@($"vault-feedback-icon icon me-2 {GetClass()}") Symbol="@GetSymbol()"></Icon>
<span class="vault-feedback-content flex-grow">
@if (Html is not null)
{
@((MarkupString)Html)
}
else if (Text is not null)
{
@Text
}
</span>
</div>
@code {
public Feedback()
{
}
public Feedback(LocalizedString str, FeedbackType state)
{
this.State = state;
this.Text = str.ToString();
}
public Feedback(LocalizedHtmlString str, FeedbackType state)
{
this.State = state;
var txt = new StringWriter();
str.WriteTo(txt, NullHtmlEncoder.Default);
this.Html = txt.ToString();
}
public FeedbackType State { get; set; }
public string GetClass()
=> State switch
{
FeedbackType.Loading => "icon-dots feedback-icon-loading",
FeedbackType.Success => "icon-checkmark feedback-icon-success",
FeedbackType.Failed => "icon-cross feedback-icon-failed",
_ => ""
};
public string GetSymbol()
=> State switch
{
FeedbackType.Loading => "dots",
FeedbackType.Success => "checkmark",
FeedbackType.Failed => "cross",
_ => ""
};
public string Html { get; set; }
public string Text { get; set; }
}

View File

@@ -0,0 +1,8 @@
namespace BTCPayServer.Blazor.VaultBridge.Elements;
public enum FeedbackType
{
Loading,
Success,
Failed
}

View File

@@ -0,0 +1,69 @@
@inherits VaultElement
@implements IDisposable
<div id="passphrase-input" class="mt-4">
<div class="form-group">
<label for="Password" class="form-label">@ui.StringLocalizer["Passphrase (Leave empty if there isn't any passphrase)"]</label>
<div class="input-group">
<input id="Password" @bind="Password" type="password" class="form-control">
<button type="button" class="btn btn-secondary px-3 only-for-js" title="@ui.StringLocalizer["Toggle passphrase visibility"]"
data-toggle-password="#Password">
<Icon Symbol="actions-show"></Icon>
</button>
</div>
</div>
<div class="form-group">
<label for="PasswordConfirmation" class="form-label">@ui.StringLocalizer["Passphrase confirmation"]</label>
<div class="input-group">
<input id="PasswordConfirmation" @bind="PasswordConfirmation" type="password" class="form-control">
<button type="button" class="btn btn-secondary px-3 only-for-js" title="@ui.StringLocalizer["Toggle passphrase visibility"]"
data-toggle-password="#PasswordConfirmation">
<Icon Symbol="actions-show"></Icon>
</button>
</div>
@if (Error != "")
{
<span class="text-danger">@Error</span>
}
</div>
<button id="vault-confirm" class="btn btn-primary mt-4" type="button" @onclick="OnConfirmPasswordClick">@ui.StringLocalizer["Confirm"]</button>
</div>
@code {
private readonly VaultBridgeUI ui;
public Passphrase(VaultBridgeUI ui)
{
this.ui = ui;
}
string PasswordConfirmation { get; set; } = "";
string Password { get; set; } = "";
string Error { get; set; } = "";
private TaskCompletionSource<string> _cts;
public Task<string> GetPassword()
{
ui.ShowFeedback(FeedbackType.Loading, ui.StringLocalizer["Enter the passphrase."]);
ui.AddElement(this);
_cts = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
return _cts.Task;
}
public void OnConfirmPasswordClick()
{
if (Password != PasswordConfirmation)
{
Error = ui.StringLocalizer["Invalid password confirmation."].Value;
ui.StateHasChanged();
return;
}
ui.Elements.Remove(this);
ui.ShowFeedback(FeedbackType.Success, ui.StringLocalizer["Password entered..."]);
_cts?.TrySetResult(Password);
_cts = null;
}
public void Dispose() => _cts?.TrySetCanceled();
}

View File

@@ -0,0 +1,104 @@
@using System.Globalization
@inherits VaultElement
@implements IDisposable
<div id="pin-input" class="mt-4">
<div class="row">
<div class="col">
<div class="input-group mb-2">
<input id="pin-display" @bind="Display" type="text" class="form-control" readonly>
<div id="pin-display-delete" class="input-group-text cursor-pointer"
@onclick="DeleteAll">
<Icon Symbol="actions-remove"></Icon>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col">
<div class="pin-button" id="pin-7" @onclick="() => Click(7)"></div>
</div>
<div class="col">
<div class="pin-button" id="pin-8" @onclick="() => Click(8)"></div>
</div>
<div class="col">
<div class="pin-button" id="pin-9" @onclick="() => Click(9)"></div>
</div>
</div>
<div class="row">
<div class="col">
<div class="pin-button" id="pin-4" @onclick="() => Click(4)"></div>
</div>
<div class="col">
<div class="pin-button" id="pin-5" @onclick="() => Click(5)"></div>
</div>
<div class="col">
<div class="pin-button" id="pin-6" @onclick="() => Click(6)"></div>
</div>
</div>
<div class="row">
<div class="col">
<div class="pin-button" id="pin-1" @onclick="() => Click(1)"></div>
</div>
<div class="col">
<div class="pin-button" id="pin-2" @onclick="() => Click(2)"></div>
</div>
<div class="col">
<div class="pin-button" id="pin-3" @onclick="() => Click(3)"></div>
</div>
</div>
</div>
<button id="vault-confirm" class="btn btn-primary mt-4" type="button" @onclick="OnConfirmPinClick">@ui.StringLocalizer["Confirm"]</button>
@code {
private readonly VaultBridgeUI ui;
public PinInput(VaultBridgeUI ui)
{
this.ui = ui;
}
public int Value => int.TryParse(input, CultureInfo.InvariantCulture, out var v) ? v : 0;
public string input = "";
public string Display = "";
public Task<int> GetPin()
{
ui.ShowFeedback(FeedbackType.Loading, ui.StringLocalizer["Enter the pin."]);
ui.AddElement(this);
_cts = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);
return _cts.Task;
}
public void Click(int i)
{
if (input.Length > 10)
return;
input += i;
UpdateDisplay();
}
private void UpdateDisplay()
{
Display = new string('*', input.Length);
ui.StateHasChanged();
}
public void DeleteAll()
{
input = "";
UpdateDisplay();
}
private TaskCompletionSource<int> _cts;
public void OnConfirmPinClick()
{
ui.Elements.Remove(this);
ui.ShowFeedback(FeedbackType.Loading, ui.StringLocalizer["Verifying pin..."]);
_cts?.TrySetResult(this.Value);
_cts = null;
}
public void Dispose() => _cts?.TrySetCanceled();
}

View File

@@ -0,0 +1,18 @@
@inherits VaultElement
<button id="vault-retry" class="btn btn-secondary mt-4" type="button" @onclick="OnRetryClick">@ui.StringLocalizer["Retry"]</button>
@code {
private readonly VaultBridgeUI ui;
public Retry(VaultBridgeUI ui)
{
this.ui = ui;
}
private async Task OnRetryClick()
{
ui.Elements.Clear();
await ui.Connect();
}
}

View File

@@ -0,0 +1,62 @@
@using BTCPayServer.Hwi
@using NBitcoin
@inherits VaultElement
@implements IDisposable
@if (ConfirmedOnDevice)
{
<button id="vault-confirm" class="btn btn-primary mt-4" type="button" @onclick="OnConfirm">@ui.StringLocalizer["Confirm"]</button>
}
else
{
<button id="vault-confirm" class="btn btn-primary mt-4" type="button" disabled="disabled">@ui.StringLocalizer["Please, confirm on the device first..."]</button>
}
@code {
private readonly VaultBridgeUI ui;
public VerifyAddress(VaultBridgeUI ui)
{
this.ui = ui;
}
public BitcoinAddress Address { get; set; }
public KeyPath KeyPath { get; set; }
public ScriptPubKeyType ScriptPubKeyType { get; set; }
public HwiDeviceClient Device { get; set; }
private TaskCompletionSource<bool> _cts;
public void OnConfirm()
{
ui.Elements.Remove(this);
ui.ShowFeedback(FeedbackType.Success, ui.StringLocalizer["Address verified."]);
_cts?.TrySetResult(true);
_cts = null;
}
bool ConfirmedOnDevice { get; set; }
public async Task<bool> WaitConfirmed()
{
ui.ShowFeedback(FeedbackType.Loading,
ui.ViewLocalizer["Please verify that the address displayed on your device is <b>{0}</b>...", Address.ToString()]);
ui.AddElement(this);
var deviceAddress = await Device.DisplayAddressAsync(ScriptPubKeyType, KeyPath, ui.CancellationToken);
// Note that the device returned here may be different from what on screen for Testnet/Regtest
if (deviceAddress != Address)
{
ui.ShowFeedback(FeedbackType.Failed, ui.StringLocalizer["Unexpected address returned by the device..."]);
return false;
}
ConfirmedOnDevice = true;
ui.StateHasChanged();
_cts = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
return await _cts.Task;
}
public void Dispose() => _cts?.TrySetCanceled();
}

View File

@@ -0,0 +1,23 @@
@using Microsoft.AspNetCore.Mvc.Localization
@inherits VaultElement
<div id="walletAlert" class="alert alert-warning alert-dismissible my-4" role="alert">
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="@ui.StringLocalizer["Close"]">
<Icon Symbol="close"></Icon>
</button>
<span id="alertMessage">
@((MarkupString)Html)
</span>
</div>
@code {
private readonly VaultBridgeUI ui;
public Warning(VaultBridgeUI ui, LocalizedHtmlString str)
{
this.ui = ui;
Html = str.Value;
}
public string Html { get; set; }
}

View File

@@ -0,0 +1,81 @@
@using NBitcoin
@inherits VaultElement
<div id="vault-xpub" class="mt-4">
<div class="form-group">
<label for="addressType" class="form-label">@ui.StringLocalizer["Address type"]</label>
<select id="addressType" @bind="AddressType" name="addressType" class="form-select w-auto">
@if (CanUseSegwit)
{
<option value="segwit">@ui.StringLocalizer["Segwit (Recommended, cheapest fee)"]</option>
<option value="segwitWrapped">@ui.StringLocalizer["Segwit wrapped (Compatible with old wallets)"]</option>
}
<option value="legacy">@ui.StringLocalizer["Legacy (Not recommended)"]</option>
@if (CanUseTaproot)
{
<option value="taproot" text-translate="true">@ui.StringLocalizer["Taproot"]</option>
}
</select>
</div>
<div class="form-group">
<label for="accountNumber" class="form-label" text-translate="true">Account</label>
<input id="accountNumber" @bind="AccountNumber" class="form-control" name="accountNumber" type="number" min="0" step="1"
style="max-width:12ch;" />
</div>
</div>
<button id="vault-confirm" class="btn btn-primary mt-4" type="button" @onclick="OnConfirmXPubClick">@ui.StringLocalizer["Confirm"]</button>
@code {
private readonly VaultBridgeUI ui;
public XPubSelect(VaultBridgeUI ui, Network network)
{
this.ui = ui;
CanUseTaproot = network.Consensus.SupportTaproot;
CanUseSegwit = network.Consensus.SupportSegwit;
AddressType = CanUseSegwit ? "segwit" : "legacy";
}
public KeyPath ToKeyPath()
=> ToScriptPubKeyType() switch
{
ScriptPubKeyType.TaprootBIP86 => new KeyPath("86'"),
ScriptPubKeyType.Segwit => new KeyPath("84'"),
ScriptPubKeyType.SegwitP2SH => new KeyPath("49'"),
_ => new KeyPath("44'"),
};
public ScriptPubKeyType ToScriptPubKeyType()
=> AddressType switch
{
"segwit" => ScriptPubKeyType.Segwit,
"segwitWrapped" => ScriptPubKeyType.SegwitP2SH,
"taproot" => ScriptPubKeyType.TaprootBIP86,
_ => ScriptPubKeyType.Legacy
};
public string AddressType { get; set; }
public int AccountNumber { get; set; }
public bool CanUseTaproot { get; }
public bool CanUseSegwit { get; }
TaskCompletionSource<XPubSelect> _cts;
public Task<XPubSelect> GetXPubSelect()
{
ui.ShowFeedback(FeedbackType.Loading, ui.StringLocalizer["Select your address type and account"]);
ui.AddElement(this);
_cts = new TaskCompletionSource<XPubSelect>(TaskCreationOptions.RunContinuationsAsynchronously);
return _cts.Task;
}
public void OnConfirmXPubClick()
{
ui.Elements.Remove(this);
ui.Elements.RemoveAt(ui.Elements.Count - 1);
ui.StateHasChanged();
_cts?.TrySetResult(this);
_cts = null;
}
public void Dispose() => _cts?.TrySetCanceled();
}

View File

@@ -0,0 +1,307 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Blazor.VaultBridge.Elements;
using BTCPayServer.Data;
using BTCPayServer.Hwi;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.JSInterop;
using NBitcoin;
using NBXplorer.DerivationStrategy;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Blazor.VaultBridge;
public abstract class HWIController : VaultController
{
override protected string VaultUri => "http://127.0.0.1:65092/hwi-bridge/v1";
public string CryptoCode { get; set; }
private static bool IsTrezorT(HwiEnumerateEntry deviceEntry)
{
return deviceEntry.Model.Contains("Trezor_T", StringComparison.OrdinalIgnoreCase);
}
private static bool IsTrezorOne(HwiEnumerateEntry deviceEntry)
{
return deviceEntry.Model.Contains("trezor_1", StringComparison.OrdinalIgnoreCase);
}
protected abstract Task Run(VaultBridgeUI ui, HwiClient hwi, HwiDeviceClient device, HDFingerprint fingerprint, BTCPayNetwork network,
CancellationToken cancellationToken);
protected override async Task Run(VaultBridgeUI ui, VaultClient vaultClient, CancellationToken cancellationToken)
{
var networkProviders = ui.ServiceProvider.GetRequiredService<BTCPayNetworkProvider>();
try
{
var network = networkProviders.GetNetwork<BTCPayNetwork>(CryptoCode);
var hwi = new Hwi.HwiClient(network.NBitcoinNetwork)
{
Transport = new VaultHWITransport(vaultClient), IgnoreInvalidNetwork = network.NBitcoinNetwork.ChainName != ChainName.Mainnet
};
ui.ShowFeedback(FeedbackType.Loading, ui.StringLocalizer["Fetching device..."]);
var version = await hwi.GetVersionAsync(cancellationToken);
if (version.Major < 2)
{
ui.ShowFeedback(FeedbackType.Failed,
ui.ViewLocalizer[
"Your BTCPay Server Vault version is outdated. Please <a target=\"_blank\" href=\"https://github.com/btcpayserver/BTCPayServer.Vault/releases/latest\">download</a> the latest version."]);
}
var gettingEntries = hwi.EnumerateEntriesAsync(cancellationToken);
var timeout = Task.Delay(TimeSpan.FromSeconds(7.0), cancellationToken);
var finished = await Task.WhenAny(gettingEntries, timeout);
// Wallets such as Trezor Safe 3 will block EnumerateEntriesAsync until password is set on the device.
// So if we wait for 7 sec and this doesn't returns, let's notify the user to look the hardware.
if (finished == timeout)
ui.ShowFeedback(FeedbackType.Loading, ui.StringLocalizer["Please, enter the passphrase on the device."]);
var entries = await gettingEntries;
var deviceEntry = entries.FirstOrDefault();
if (deviceEntry is null)
{
ui.ShowFeedback(FeedbackType.Failed, ui.StringLocalizer["No device connected."]);
ui.ShowRetry();
return;
}
if (deviceEntry.Model is null)
{
ui.ShowFeedback(FeedbackType.Failed,
ui.StringLocalizer["Unsupported hardware wallet, try to update BTCPay Server Vault"]);
ui.ShowRetry();
return;
}
var device = new HwiDeviceClient(hwi, deviceEntry.DeviceSelector, deviceEntry.Model, deviceEntry.Fingerprint);
ui.ShowFeedback(FeedbackType.Success, ui.StringLocalizer["Device found: {0}", device.GetNiceModelName()]);
HDFingerprint? fingerprint = deviceEntry.Fingerprint;
bool dirtyDevice = false;
if (deviceEntry is { Code: HwiErrorCode.DeviceNotReady })
{
// It seems that this 'if (IsTrezorT(deviceEntry))' can be removed.
// I have not managed to trigger this anymore with latest 2.8.9
// the passphrase is getting asked during EnumerateEntriesAsync
if (IsTrezorT(deviceEntry))
{
ui.ShowFeedback(FeedbackType.Loading, ui.StringLocalizer["Please, enter the passphrase on the device."]);
// The make the trezor T ask for password
await device.GetXPubAsync(new KeyPath("44'"), cancellationToken);
dirtyDevice = true;
}
else if (deviceEntry.NeedsPinSent is true)
{
await device.PromptPinAsync(cancellationToken);
var pinElement = new PinInput(ui);
var pin = await pinElement.GetPin();
if (!await device.SendPinAsync(pin, cancellationToken))
{
ui.ShowFeedback(FeedbackType.Failed, ui.StringLocalizer["Incorrect pin code."]);
ui.ShowRetry();
return;
}
ui.ShowFeedback(FeedbackType.Success, ui.StringLocalizer["Pin code verified."]);
dirtyDevice = true;
}
}
else if (deviceEntry is { Code: HwiErrorCode.DeviceNotInitialized })
{
ui.ShowFeedback(FeedbackType.Failed, ui.StringLocalizer["The device has not been initialized."]);
ui.ShowRetry();
return;
}
if (IsTrezorOne(deviceEntry) && HasPassphraseProtection(deviceEntry))
{
var passwordEl = new Passphrase(ui);
device.Password = await passwordEl.GetPassword();
if (!string.IsNullOrEmpty(device.Password))
{
device = new HwiDeviceClient(hwi, DeviceSelectors.FromDeviceType("trezor", deviceEntry.Path), deviceEntry.Model, null)
{
Password = device.Password
};
fingerprint = null;
}
}
if (dirtyDevice)
{
entries = (await hwi.EnumerateEntriesAsync(cancellationToken)).ToList();
deviceEntry = entries.FirstOrDefault() ?? deviceEntry;
device = new HwiDeviceClient(hwi, deviceEntry.DeviceSelector, deviceEntry.Model, deviceEntry.Fingerprint);
fingerprint = deviceEntry.Fingerprint;
}
if (fingerprint is null)
{
ui.ShowFeedback(FeedbackType.Loading, ui.StringLocalizer["Fetching wallet's fingerprint."]);
fingerprint = (await device.GetXPubAsync(new KeyPath("44'"), ui.CancellationToken)).ExtPubKey.ParentFingerprint;
device = new HwiDeviceClient(hwi, DeviceSelectors.FromFingerprint(fingerprint.Value), deviceEntry.Model, fingerprint)
{
Password = device.Password
};
ui.ShowFeedback(FeedbackType.Success, ui.StringLocalizer["Wallet's fingerprint fetched."]);
}
await Run(ui, hwi, device, fingerprint.Value, network, cancellationToken);
}
catch (HwiException e)
{
var message = e switch
{
{ ErrorCode: HwiErrorCode.ActionCanceled } => ui.StringLocalizer["Action canceled by user"],
_ => ui.StringLocalizer["An unexpected error happened: {0}", $"{e.Message} ({e.ErrorCode})"],
};
ui.ShowFeedback(FeedbackType.Failed, message);
ui.ShowRetry();
}
}
private bool HasPassphraseProtection(HwiEnumerateEntry deviceEntry)
{
if (deviceEntry.NeedsPassphraseSent is true)
return true;
if (deviceEntry.RawData["warnings"] is JArray arr)
{
return arr.Any(e => e.ToString().Contains("passphrase was not provided", StringComparison.OrdinalIgnoreCase));
}
return false;
}
}
public class SignHWIController : HWIController
{
public string StoreId { get; set; }
public string PSBT { get; set; }
protected override async Task Run(VaultBridgeUI ui, HwiClient hwi, HwiDeviceClient device, HDFingerprint fingerprint, BTCPayNetwork network,
CancellationToken cancellationToken)
{
if (!NBitcoin.PSBT.TryParse(PSBT, network.NBitcoinNetwork, out var psbt))
return;
var store = await ui.ServiceProvider.GetRequiredService<StoreRepository>().FindStore(StoreId ?? "");
var handlers = ui.ServiceProvider.GetRequiredService<PaymentMethodHandlerDictionary>();
var pmi = Payments.PaymentTypes.CHAIN.GetPaymentMethodId(network.CryptoCode);
var derivationSettings = store?.GetPaymentMethodConfig<DerivationSchemeSettings>(pmi, handlers);
if (store is null || derivationSettings is null)
return;
ui.ShowFeedback(FeedbackType.Loading, ui.StringLocalizer["Checking if this device can sign the transaction..."]);
// we ensure that the device fingerprint is part of the derivation settings
if (derivationSettings.AccountKeySettings.All(a => a.RootFingerprint != fingerprint))
{
ui.ShowFeedback(FeedbackType.Failed,
ui.StringLocalizer[
"This device can't sign the transaction. (Wrong device, wrong passphrase or wrong device fingerprint in your wallet settings)"]);
ui.ShowRetry();
return;
}
derivationSettings.RebaseKeyPaths(psbt);
// otherwise, let the device check if it can sign anything
var signableInputs = psbt.Inputs
.SelectMany(i => i.HDKeyPaths)
.Where(i => i.Value.MasterFingerprint == fingerprint)
.ToArray();
if (signableInputs.Length > 0)
{
var actualPubKey = (await device.GetXPubAsync(signableInputs[0].Value.KeyPath, cancellationToken)).GetPublicKey();
if (actualPubKey != signableInputs[0].Key)
{
ui.ShowFeedback(FeedbackType.Failed,
ui.StringLocalizer["This device can't sign the transaction. (The wallet keypath in your wallet settings seems incorrect)"]);
ui.ShowRetry();
return;
}
if (derivationSettings.IsMultiSigOnServer)
{
var alreadySigned = psbt.Inputs.Any(a =>
a.PartialSigs.Any(a => a.Key == actualPubKey));
if (alreadySigned)
{
ui.ShowFeedback(FeedbackType.Failed, ui.StringLocalizer["This device already signed PSBT."]);
ui.ShowRetry();
return;
}
}
}
ui.ShowFeedback(FeedbackType.Loading,
ui.StringLocalizer["Please review and confirm the transaction on your device..."]);
psbt = await device.SignPSBTAsync(psbt, cancellationToken);
ui.ShowFeedback(FeedbackType.Loading, ui.StringLocalizer["Transaction signed successfully, proceeding to review..."]);
await ui.JSRuntime.InvokeVoidAsync("vault.setSignedPSBT", cancellationToken, new System.Text.Json.Nodes.JsonObject() { ["psbt"] = psbt.ToBase64() });
}
}
public class GetXPubController : HWIController
{
protected override async Task Run(VaultBridgeUI ui, HwiClient hwi, HwiDeviceClient device, HDFingerprint fingerprint, BTCPayNetwork network,
CancellationToken cancellationToken)
{
var xpubSelect = new XPubSelect(ui, network.NBitcoinNetwork);
var xpubInfo = await xpubSelect.GetXPubSelect();
var scriptPubKeyTypeType = xpubInfo.ToScriptPubKeyType();
ui.ShowFeedback(FeedbackType.Loading, ui.StringLocalizer["Fetching public keys..."]);
KeyPath keyPath = xpubInfo.ToKeyPath().Derive(network.CoinType).Derive(xpubInfo.AccountNumber, true);
BitcoinExtPubKey xpub = await device.GetXPubAsync(keyPath, cancellationToken);
var factory = network.NBXplorerNetwork.DerivationStrategyFactory;
var strategy = factory.CreateDirectDerivationStrategy(xpub, new DerivationStrategyOptions() { ScriptPubKeyType = scriptPubKeyTypeType });
ui.ShowFeedback(FeedbackType.Success, ui.StringLocalizer["Public keys successfully fetched."]);
var firstDepositPath = new KeyPath(0, 0);
var firstDepositAddr =
network.NBXplorerNetwork.CreateAddress(strategy, firstDepositPath, strategy.GetDerivation(firstDepositPath).ScriptPubKey);
ui.ShowFeedback(FeedbackType.Loading,
ui.ViewLocalizer["Please verify that the address displayed on your device is <b>{0}</b>...", firstDepositAddr.ToString()]);
var verif = new VerifyAddress(ui)
{
Device = device,
KeyPath = keyPath.Derive(firstDepositPath),
Address = firstDepositAddr,
ScriptPubKeyType = xpubInfo.ToScriptPubKeyType()
};
if (!await verif.WaitConfirmed())
{
ui.ShowRetry();
return;
}
ui.ShowFeedback(FeedbackType.Loading, ui.StringLocalizer["Saving..."]);
var settings = new DerivationSchemeSettings(strategy, network) { Source = "Vault" };
settings.AccountKeySettings[0].AccountKeyPath = keyPath;
settings.AccountKeySettings[0].RootFingerprint = fingerprint;
string[] mandatoryPrevUtxo = ["trezor", "jade"];
settings.DefaultIncludeNonWitnessUtxo = (device.Model, scriptPubKeyTypeType) switch
{
(_, ScriptPubKeyType.TaprootBIP86) => false,
(_, ScriptPubKeyType.Legacy) => true,
({ } s, _) when mandatoryPrevUtxo.Any(o => s.Contains(o, StringComparison.OrdinalIgnoreCase)) => true,
_ => false,
};
settings.Label = $"{device.GetNiceModelName()} ({fingerprint})";
var handlers = ui.ServiceProvider.GetRequiredService<PaymentMethodHandlerDictionary>();
var handler = handlers.GetBitcoinHandler(network.CryptoCode);
var dataProtector = ui.ServiceProvider.GetRequiredService<IDataProtectionProvider>().CreateProtector("ConfigProtector");
await ui.JSRuntime.InvokeVoidAsync("vault.setXPub", cancellationToken,
new System.Text.Json.Nodes.JsonObject() { ["config"] = dataProtector.ProtectString(JToken.FromObject(settings, handler.Serializer).ToString()) });
}
}

View File

@@ -0,0 +1,9 @@
using System.Threading;
using System.Threading.Tasks;
namespace BTCPayServer.Blazor.VaultBridge;
public interface IController
{
Task Run(VaultBridgeUI ui, CancellationToken cancellationToken);
}

View File

@@ -0,0 +1,146 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Blazor.VaultBridge.Elements;
using BTCPayServer.Data;
using BTCPayServer.NTag424;
using BTCPayServer.Services;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.JSInterop;
using static BTCPayServer.BoltcardDataExtensions;
namespace BTCPayServer.Blazor.VaultBridge;
record CardOrigin
{
public record Blank() : CardOrigin;
public record ThisIssuer(BoltcardRegistration Registration) : CardOrigin;
public record ThisIssuerConfigured(string PullPaymentId, BoltcardRegistration Registration) : ThisIssuer(Registration);
public record OtherIssuer() : CardOrigin;
public record ThisIssuerReset(BoltcardRegistration Registration) : ThisIssuer(Registration);
}
public class NFCController : VaultController
{
protected override string VaultUri => "http://127.0.0.1:65092/nfc-bridge/v1";
public bool NewCard { get; set; }
public string PullPaymentId { get; set; }
public string BoltcardUrl { get; set; }
protected override async Task Run(VaultBridgeUI ui, VaultClient vaultClient, CancellationToken cancellationToken)
{
try
{
ui.ShowFeedback(FeedbackType.Loading, ui.StringLocalizer["Waiting for NFC to be presented..."]);
var transport = new APDUVaultTransport(vaultClient);
var ntag = new Ntag424(transport);
await transport.WaitForCard(cancellationToken);
ui.ShowFeedback(FeedbackType.Success, ui.StringLocalizer["NFC detected."]);
var settingsRepository = ui.ServiceProvider.GetRequiredService<SettingsRepository>();
var env = ui.ServiceProvider.GetRequiredService<BTCPayServerEnvironment>();
var issuerKey = await settingsRepository.GetIssuerKey(env);
var dbContextFactory = ui.ServiceProvider.GetRequiredService<ApplicationDbContextFactory>();
CardOrigin cardOrigin = await GetCardOrigin(dbContextFactory, PullPaymentId, ntag, issuerKey, cancellationToken);
if (cardOrigin is CardOrigin.OtherIssuer)
{
ui.ShowFeedback(FeedbackType.Failed, ui.StringLocalizer["This card is already configured for another issuer"]);
ui.ShowRetry();
return;
}
if (NewCard)
{
ui.ShowFeedback(FeedbackType.Loading, ui.StringLocalizer["Configuring Boltcard..."]);
if (cardOrigin is CardOrigin.Blank || cardOrigin is CardOrigin.ThisIssuerReset)
{
await ntag.AuthenticateEV2First(0, AESKey.Default, cancellationToken);
var uid = await ntag.GetCardUID(cancellationToken);
try
{
var version = await dbContextFactory.LinkBoltcardToPullPayment(PullPaymentId, issuerKey, uid);
var cardKey = issuerKey.CreatePullPaymentCardKey(uid, version, PullPaymentId);
await ntag.SetupBoltcard(BoltcardUrl, BoltcardKeys.Default, cardKey.DeriveBoltcardKeys(issuerKey));
}
catch
{
await dbContextFactory.SetBoltcardResetState(issuerKey, uid);
throw;
}
ui.ShowFeedback(FeedbackType.Success, ui.StringLocalizer["The card is now configured"]);
}
else if (cardOrigin is CardOrigin.ThisIssuer)
{
ui.ShowFeedback(FeedbackType.Success, ui.StringLocalizer["This card is already properly configured"]);
}
}
else
{
ui.ShowFeedback(FeedbackType.Loading, ui.StringLocalizer["Resetting Boltcard..."]);
if (cardOrigin is CardOrigin.Blank)
{
ui.ShowFeedback(FeedbackType.Success, ui.StringLocalizer["This card is already in a factory state"]);
}
else if (cardOrigin is CardOrigin.ThisIssuer thisIssuer)
{
var cardKey = issuerKey.CreatePullPaymentCardKey(thisIssuer.Registration.UId, thisIssuer.Registration.Version, PullPaymentId);
await ntag.ResetCard(issuerKey, cardKey);
await dbContextFactory.SetBoltcardResetState(issuerKey, thisIssuer.Registration.UId);
ui.ShowFeedback(FeedbackType.Success, ui.StringLocalizer["Card reset succeed"]);
}
}
ui.ShowFeedback(FeedbackType.Loading, ui.StringLocalizer["Please remove the NFC from the card reader"]);
await transport.WaitForRemoved(cancellationToken);
ui.ShowFeedback(FeedbackType.Success, ui.StringLocalizer["Thank you!"]);
}
catch (UnexpectedResponseException e)
{
ui.ShowFeedback(FeedbackType.Failed, ui.StringLocalizer["An unexpected error happened: {0}", e.Message]);
ui.ShowRetry();
return;
}
// Give them time to read the message
await Task.Delay(1000, cancellationToken);
await ui.JSRuntime.InvokeVoidAsync("vault.done", cancellationToken);
}
private async Task<CardOrigin> GetCardOrigin(ApplicationDbContextFactory dbContextFactory, string pullPaymentId, Ntag424 ntag, IssuerKey issuerKey,
CancellationToken cancellationToken)
{
CardOrigin cardOrigin;
Uri uri = await ntag.TryReadNDefURI(cancellationToken);
if (uri is null)
{
cardOrigin = new CardOrigin.Blank();
}
else
{
var piccData = issuerKey.TryDecrypt(uri);
if (piccData is null)
{
cardOrigin = new CardOrigin.OtherIssuer();
}
else
{
var res = await dbContextFactory.GetBoltcardRegistration(issuerKey, piccData.Uid);
if (res != null && res.PullPaymentId is null)
cardOrigin = new CardOrigin.ThisIssuerReset(res);
else if (res?.PullPaymentId != pullPaymentId)
cardOrigin = new CardOrigin.OtherIssuer();
else
cardOrigin = new CardOrigin.ThisIssuerConfigured(res.PullPaymentId, res);
}
}
return cardOrigin;
}
}

View File

@@ -0,0 +1,122 @@
@using System.Threading
@using BTCPayServer.Blazor.VaultBridge.Elements
@using Microsoft.AspNetCore.Mvc.Localization
@using Microsoft.Extensions.Localization
@inject IStringLocalizer stringLocalizer
@inject ViewLocalizer viewLocalizer
@inject IJSRuntime jsRuntime
@inject IServiceProvider serviceProvider
@implements IDisposable
@foreach (var e in this.Elements)
{
@e.RenderFragment
}
@code {
[Parameter] public IController Controller { get; set; }
public List<VaultElement> Elements { get; set; } = new List<VaultElement>();
public new void StateHasChanged()
{
base.StateHasChanged();
}
public async Task Connect()
{
try
{
await Controller.Run(this, CancellationToken);
}
catch when (CancellationToken.IsCancellationRequested)
{
}
catch (Exception e)
{
this.ShowFeedback(FeedbackType.Failed, StringLocalizer["An unexpected error happened: {0}", e.Message]);
if (e is VaultClient.VaultException)
ShowRetry();
else
throw;
}
}
public void ShowRetry()
{
if (!Elements.OfType<Retry>().Any())
{
Elements.Add(new Retry(this));
this.StateHasChanged();
}
}
public void AddWarning(LocalizedHtmlString str)
{
Elements.Insert(0, new Warning(this, str));
this.StateHasChanged();
}
protected override async Task OnInitializedAsync()
{
await Connect();
}
public void ShowFeedback(FeedbackType state, LocalizedString str)
{
var feedback = new Feedback(str, state);
ShowFeedback(feedback);
}
public void ShowFeedback(FeedbackType state, LocalizedHtmlString str)
{
var feedback = new Feedback(str, state);
ShowFeedback(feedback);
}
public void ShowFeedback(Feedback feedback)
{
var lastFeedback =(Feedback)(Elements.FindLastIndex(e => e is Feedback) switch
{
int i when i >= 0 => Elements[i],
_ => null
});
if (lastFeedback is null || lastFeedback.State == FeedbackType.Success)
{
Elements.Add(feedback);
}
else
{
// Replace the last non successful feedback by the new one
for (int i = Elements.Count - 1; i >= 0; i--)
{
if (Elements[i] == lastFeedback)
break;
Elements.RemoveAt(i);
}
Elements[^1] = feedback;
}
this.StateHasChanged();
}
public void AddElement(VaultElement el)
{
Elements.Add(el);
this.StateHasChanged();
}
CancellationTokenSource _Cts = new CancellationTokenSource();
public CancellationToken CancellationToken => _Cts.Token;
public IStringLocalizer StringLocalizer => stringLocalizer;
public ViewLocalizer ViewLocalizer => viewLocalizer;
public IJSRuntime JSRuntime => jsRuntime;
public IServiceProvider ServiceProvider => serviceProvider;
public void Dispose()
{
_Cts.Cancel();
foreach (var el in Elements.OfType<IDisposable>())
{
el.Dispose();
}
}
}

View File

@@ -0,0 +1,55 @@
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Blazor.VaultBridge.Elements;
namespace BTCPayServer.Blazor.VaultBridge;
public abstract class VaultController : IController
{
protected abstract string VaultUri { get; }
protected abstract Task Run(VaultBridgeUI ui, VaultClient vaultClient, CancellationToken cancellationToken);
public async Task Run(VaultBridgeUI ui, CancellationToken cancellationToken)
{
try
{
ui.ShowFeedback(FeedbackType.Loading, ui.StringLocalizer["Checking BTCPay Server Vault is running..."]);
var client = new VaultClient(ui.JSRuntime, VaultUri);
var res = await client.AskPermission(cancellationToken);
var feedback = (status: res.HttpCode, browser: res.Browser) switch
{
(200, _) => new Feedback(ui.StringLocalizer["Access to vault granted by owner."], FeedbackType.Success),
(401, _) => new Feedback(ui.StringLocalizer["The user declined access to the vault."],
FeedbackType.Failed),
(_, "safari") => new Feedback(
ui.ViewLocalizer[
"Safari doesn't support BTCPay Server Vault. Please use a different browser. (<a class=\"alert-link\" href=\"https://bugs.webkit.org/show_bug.cgi?id=171934\" target=\"_blank\" rel=\"noreferrer noopener\">More information</a>)"],
FeedbackType.Failed),
_ => new Feedback(
ui.ViewLocalizer[
"BTCPay Server Vault does not seem to be running, you can download it on <a target=\"_blank\" href=\"https://github.com/btcpayserver/BTCPayServer.Vault/releases/latest\">Github</a>."],
FeedbackType.Failed),
};
ui.ShowFeedback(feedback);
if (res.HttpCode != 200)
{
if (res.HttpCode == 0 && res.Browser == "brave")
ui.AddWarning(ui.ViewLocalizer[
"Brave supports BTCPay Server Vault, but you need to disable Brave Shields. (<a class=\"alert-link\" href=\"https://www.updateland.com/how-to-turn-off-brave-shields/\" target=\"_blank\" rel=\"noreferrer noopener\">More information</a>)"]);
ui.ShowRetry();
return;
}
await Run(ui, client, cancellationToken);
}
catch (VaultClient.VaultNotConnectedException)
{
ui.ShowFeedback(new Feedback(
ui.ViewLocalizer[
"BTCPay Server Vault does not seem to be running, you can download it on <a target=\"_blank\" href=\"https://github.com/btcpayserver/BTCPayServer.Vault/releases/latest\">Github</a>."],
FeedbackType.Failed));
ui.ShowRetry();
}
}
}

View File

@@ -0,0 +1,11 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
namespace BTCPayServer.Blazor.VaultBridge;
public class VaultElement
{
protected virtual void BuildRenderTree(RenderTreeBuilder builder) { }
public RenderFragment RenderFragment => BuildRenderTree;
}

View File

@@ -1019,7 +1019,7 @@ namespace BTCPayServer.Controllers
leases.Add(_EventAggregator.SubscribeAsync<Events.InvoiceEvent>(async o => await NotifySocket(webSocket, o.Invoice.Id, invoiceId))); leases.Add(_EventAggregator.SubscribeAsync<Events.InvoiceEvent>(async o => await NotifySocket(webSocket, o.Invoice.Id, invoiceId)));
while (true) while (true)
{ {
var message = await webSocket.ReceiveAndPingAsync(DummyBuffer); var message = await webSocket.ReceiveAndPingAsync(DummyBuffer, cancellationToken);
if (message.MessageType == WebSocketMessageType.Close) if (message.MessageType == WebSocketMessageType.Close)
break; break;
} }

View File

@@ -1,16 +1,7 @@
using BTCPayServer.Abstractions.Constants; using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Lightning;
using BTCPayServer.Models; using BTCPayServer.Models;
using BTCPayServer.NTag424;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using System;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Html;
using static BTCPayServer.BoltcardDataExtensions;
namespace BTCPayServer.Controllers namespace BTCPayServer.Controllers
{ {
@@ -20,13 +11,16 @@ namespace BTCPayServer.Controllers
[HttpGet("pull-payments/{pullPaymentId}/boltcard/{command}")] [HttpGet("pull-payments/{pullPaymentId}/boltcard/{command}")]
public IActionResult SetupBoltcard(string pullPaymentId, string command) public IActionResult SetupBoltcard(string pullPaymentId, string command)
{ {
return View(nameof(SetupBoltcard), new SetupBoltcardViewModel return View(nameof(SetupBoltcard),
{ new SetupBoltcardViewModel
ReturnUrl = Url.Action(nameof(ViewPullPayment), "UIPullPayment", new { pullPaymentId }), {
WebsocketPath = Url.Action(nameof(VaultNFCBridgeConnection), "UIPullPayment", new { pullPaymentId }), ReturnUrl = Url.Action(nameof(ViewPullPayment), "UIPullPayment", new { pullPaymentId }),
Command = command BoltcardUrl = Url.ActionAbsolute(this.Request, nameof(UIBoltcardController.GetWithdrawRequest), "UIBoltcard").AbsoluteUri,
}); NewCard = command == "configure-boltcard",
PullPaymentId = pullPaymentId
});
} }
[AllowAnonymous] [AllowAnonymous]
[HttpPost("pull-payments/{pullPaymentId}/boltcard/{command}")] [HttpPost("pull-payments/{pullPaymentId}/boltcard/{command}")]
public IActionResult SetupBoltcardPost(string pullPaymentId, string command) public IActionResult SetupBoltcardPost(string pullPaymentId, string command)
@@ -34,166 +28,5 @@ namespace BTCPayServer.Controllers
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Boltcard is configured"].Value; TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Boltcard is configured"].Value;
return RedirectToAction(nameof(ViewPullPayment), new { pullPaymentId }); return RedirectToAction(nameof(ViewPullPayment), new { pullPaymentId });
} }
record CardOrigin
{
public record Blank() : CardOrigin;
public record ThisIssuer(BoltcardRegistration Registration) : CardOrigin;
public record ThisIssuerConfigured(string PullPaymentId, BoltcardRegistration Registration) : ThisIssuer(Registration);
public record OtherIssuer() : CardOrigin;
public record ThisIssuerReset(BoltcardRegistration Registration) : ThisIssuer(Registration);
}
[Route("pull-payments/{pullPaymentId}/nfc/bridge")]
public async Task<IActionResult> VaultNFCBridgeConnection(string pullPaymentId)
{
if (!HttpContext.WebSockets.IsWebSocketRequest)
return NotFound();
var pp = await _pullPaymentHostedService.GetPullPayment(pullPaymentId, false);
if (pp is null)
return NotFound();
if (!_pullPaymentHostedService.SupportsLNURL(pp))
return BadRequest();
var boltcardUrl = Url.Action(nameof(UIBoltcardController.GetWithdrawRequest), "UIBoltcard");
boltcardUrl = Request.GetAbsoluteUri(boltcardUrl);
var websocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
var vaultClient = new VaultClient(websocket);
var transport = new APDUVaultTransport(vaultClient);
var ntag = new Ntag424(transport);
using (var cts = new CancellationTokenSource(TimeSpan.FromMinutes(10)))
{
next:
while (websocket.State == System.Net.WebSockets.WebSocketState.Open)
{
try
{
var command = await vaultClient.GetNextCommand(cts.Token);
var permission = await vaultClient.AskPermission(VaultServices.NFC, cts.Token);
if (permission is null)
{
await vaultClient.Show(VaultMessageType.Error, StringLocalizer["BTCPay Server Vault does not seem to be running, you can download it on {0}.", new HtmlString("<a href=\"https://github.com/btcpayserver/BTCPayServer.Vault/releases/latest/\" class=\"alert-link\" target=\"_blank\" rel=\"noreferrer noopener\">GitHub</a>")], cts.Token);
goto next;
}
await vaultClient.Show(VaultMessageType.Ok, StringLocalizer["BTCPayServer successfully connected to the vault."], cts.Token);
if (permission is false)
{
await vaultClient.Show(VaultMessageType.Error, StringLocalizer["The user declined access to the vault."], cts.Token);
goto next;
}
await vaultClient.Show(VaultMessageType.Ok, StringLocalizer["Access to vault granted by owner."], cts.Token);
await vaultClient.Show(VaultMessageType.Processing, StringLocalizer["Waiting for NFC to be presented..."], cts.Token);
await transport.WaitForCard(cts.Token);
await vaultClient.Show(VaultMessageType.Ok, StringLocalizer["NFC detected."], cts.Token);
var issuerKey = await _settingsRepository.GetIssuerKey(_env);
CardOrigin cardOrigin = await GetCardOrigin(pullPaymentId, ntag, issuerKey, cts.Token);
if (cardOrigin is CardOrigin.OtherIssuer)
{
await vaultClient.Show(VaultMessageType.Error, StringLocalizer["This card is already configured for another issuer"], cts.Token);
goto next;
}
bool success = false;
switch (command)
{
case "configure-boltcard":
await vaultClient.Show(VaultMessageType.Processing, StringLocalizer["Configuring Boltcard..."], cts.Token);
if (cardOrigin is CardOrigin.Blank || cardOrigin is CardOrigin.ThisIssuerReset)
{
await ntag.AuthenticateEV2First(0, AESKey.Default, cts.Token);
var uid = await ntag.GetCardUID();
try
{
var version = await _dbContextFactory.LinkBoltcardToPullPayment(pullPaymentId, issuerKey, uid);
var cardKey = issuerKey.CreatePullPaymentCardKey(uid, version, pullPaymentId);
await ntag.SetupBoltcard(boltcardUrl, BoltcardKeys.Default, cardKey.DeriveBoltcardKeys(issuerKey));
}
catch
{
await _dbContextFactory.SetBoltcardResetState(issuerKey, uid);
throw;
}
await vaultClient.Show(VaultMessageType.Ok, StringLocalizer["The card is now configured"], cts.Token);
}
else if (cardOrigin is CardOrigin.ThisIssuer)
{
await vaultClient.Show(VaultMessageType.Ok, StringLocalizer["This card is already properly configured"], cts.Token);
}
success = true;
break;
case "reset-boltcard":
await vaultClient.Show(VaultMessageType.Processing, StringLocalizer["Resetting Boltcard..."], cts.Token);
if (cardOrigin is CardOrigin.Blank)
{
await vaultClient.Show(VaultMessageType.Ok, StringLocalizer["This card is already in a factory state"], cts.Token);
}
else if (cardOrigin is CardOrigin.ThisIssuer thisIssuer)
{
var cardKey = issuerKey.CreatePullPaymentCardKey(thisIssuer.Registration.UId, thisIssuer.Registration.Version, pullPaymentId);
await ntag.ResetCard(issuerKey, cardKey);
await _dbContextFactory.SetBoltcardResetState(issuerKey, thisIssuer.Registration.UId);
await vaultClient.Show(VaultMessageType.Ok, StringLocalizer["Card reset succeed"], cts.Token);
}
success = true;
break;
}
if (success)
{
await vaultClient.Show(VaultMessageType.Processing, StringLocalizer["Please remove the NFC from the card reader"], cts.Token);
await transport.WaitForRemoved(cts.Token);
await vaultClient.Show(VaultMessageType.Ok, StringLocalizer["Thank you!"], cts.Token);
await vaultClient.SendSimpleMessage("done", cts.Token);
}
}
catch (Exception) when (websocket.State != WebSocketState.Open || cts.IsCancellationRequested)
{
await WebsocketHelper.CloseSocket(websocket);
}
catch (Exception ex)
{
try
{
await vaultClient.Show(VaultMessageType.Error, StringLocalizer["Unexpected error: {0}", ex.Message], ex.ToString(), cts.Token);
}
catch { }
}
}
}
return new EmptyResult();
}
private async Task<CardOrigin> GetCardOrigin(string pullPaymentId, Ntag424 ntag, IssuerKey issuerKey, CancellationToken cancellationToken)
{
CardOrigin cardOrigin;
Uri uri = await ntag.TryReadNDefURI(cancellationToken);
if (uri is null)
{
cardOrigin = new CardOrigin.Blank();
}
else
{
var piccData = issuerKey.TryDecrypt(uri);
if (piccData is null)
{
cardOrigin = new CardOrigin.OtherIssuer();
}
else
{
var res = await _dbContextFactory.GetBoltcardRegistration(issuerKey, piccData.Uid);
if (res != null && res.PullPaymentId is null)
cardOrigin = new CardOrigin.ThisIssuerReset(res);
else if (res?.PullPaymentId != pullPaymentId)
cardOrigin = new CardOrigin.OtherIssuer();
else
cardOrigin = new CardOrigin.ThisIssuerConfigured(res.PullPaymentId, res);
}
}
return cardOrigin;
}
} }
} }

View File

@@ -152,7 +152,7 @@ public partial class UIStoresController
{ {
try try
{ {
strategy = handler.ParsePaymentMethodConfig(JToken.Parse(UnprotectString(vm.Config))); strategy = handler.ParsePaymentMethodConfig(JToken.Parse(_dataProtector.UnprotectString(vm.Config)));
} }
catch catch
{ {
@@ -167,7 +167,7 @@ public partial class UIStoresController
return View(vm.ViewName, vm); return View(vm.ViewName, vm);
} }
vm.Config = ProtectString(JToken.FromObject(strategy, handler.Serializer).ToString()); vm.Config = _dataProtector.ProtectString(JToken.FromObject(strategy, handler.Serializer).ToString());
ModelState.Remove(nameof(vm.Config)); ModelState.Remove(nameof(vm.Config));
var storeBlob = store.GetStoreBlob(); var storeBlob = store.GetStoreBlob();
@@ -197,15 +197,6 @@ public partial class UIStoresController
return ConfirmAddresses(vm, strategy, network.NBXplorerNetwork); return ConfirmAddresses(vm, strategy, network.NBXplorerNetwork);
} }
private string ProtectString(string str)
{
return Convert.ToBase64String(_dataProtector.Protect(Encoding.UTF8.GetBytes(str)));
}
private string UnprotectString(string str)
{
return Encoding.UTF8.GetString(_dataProtector.Unprotect(Convert.FromBase64String(str)));
}
[HttpGet("{storeId}/onchain/{cryptoCode}/generate/{method?}")] [HttpGet("{storeId}/onchain/{cryptoCode}/generate/{method?}")]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public async Task<IActionResult> GenerateWallet(WalletSetupViewModel vm) public async Task<IActionResult> GenerateWallet(WalletSetupViewModel vm)
@@ -278,7 +269,6 @@ public partial class UIStoresController
Network = network, Network = network,
Source = isImport ? "SeedImported" : "NBXplorerGenerated", Source = isImport ? "SeedImported" : "NBXplorerGenerated",
IsHotWallet = isImport ? request.SavePrivateKeys : method == WalletSetupMethod.HotWallet, IsHotWallet = isImport ? request.SavePrivateKeys : method == WalletSetupMethod.HotWallet,
DerivationSchemeFormat = "BTCPay",
SupportTaproot = network.NBitcoinNetwork.Consensus.SupportTaproot, SupportTaproot = network.NBitcoinNetwork.Consensus.SupportTaproot,
SupportSegwit = network.NBitcoinNetwork.Consensus.SupportSegwit SupportSegwit = network.NBitcoinNetwork.Consensus.SupportSegwit
}; };
@@ -329,7 +319,7 @@ public partial class UIStoresController
vm.RootFingerprint = response.AccountKeyPath.MasterFingerprint.ToString(); vm.RootFingerprint = response.AccountKeyPath.MasterFingerprint.ToString();
vm.AccountKey = response.AccountHDKey.Neuter().ToWif(); vm.AccountKey = response.AccountHDKey.Neuter().ToWif();
vm.KeyPath = response.AccountKeyPath.KeyPath.ToString(); vm.KeyPath = response.AccountKeyPath.KeyPath.ToString();
vm.Config = ProtectString(JToken.FromObject(derivationSchemeSettings, handler.Serializer).ToString()); vm.Config = _dataProtector.ProtectString(JToken.FromObject(derivationSchemeSettings, handler.Serializer).ToString());
var result = await UpdateWallet(vm); var result = await UpdateWallet(vm);
@@ -434,7 +424,7 @@ public partial class UIStoresController
MasterFingerprint = e.RootFingerprint is { } fp ? fp.ToString() : null, MasterFingerprint = e.RootFingerprint is { } fp ? fp.ToString() : null,
AccountKeyPath = e.AccountKeyPath == null ? "" : $"m/{e.AccountKeyPath}" AccountKeyPath = e.AccountKeyPath == null ? "" : $"m/{e.AccountKeyPath}"
}).ToList(), }).ToList(),
Config = ProtectString(JToken.FromObject(derivation, handler.Serializer).ToString()), Config = _dataProtector.ProtectString(JToken.FromObject(derivation, handler.Serializer).ToString()),
PayJoinEnabled = storeBlob.PayJoinEnabled, PayJoinEnabled = storeBlob.PayJoinEnabled,
CanUsePayJoin = perm.CanCreateHotWallet && network.SupportPayJoin && derivation.IsHotWallet, CanUsePayJoin = perm.CanCreateHotWallet && network.SupportPayJoin && derivation.IsHotWallet,
CanUseHotWallet = perm.CanCreateHotWallet, CanUseHotWallet = perm.CanCreateHotWallet,
@@ -717,8 +707,8 @@ public partial class UIStoresController
var derivation = line.Derive(i); var derivation = line.Derive(i);
var address = network.CreateAddress(strategy.AccountDerivation, var address = network.CreateAddress(strategy.AccountDerivation,
line.KeyPathTemplate.GetKeyPath(i), line.KeyPathTemplate.GetKeyPath(i),
derivation.ScriptPubKey).ToString(); derivation.ScriptPubKey);
vm.AddressSamples.Add((keyPath.ToString(), address, rootedKeyPath)); vm.AddressSamples.Add((keyPath.ToString(), address.ToString(), rootedKeyPath));
} }
} }

View File

@@ -1,432 +0,0 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Client;
using BTCPayServer.Data;
using BTCPayServer.Hwi;
using BTCPayServer.ModelBinders;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Services.Invoices;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using NBitcoin;
using NBXplorer.DerivationStrategy;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Controllers
{
[Route("vault")]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie, Policy = Policies.CanModifyStoreSettings)]
public class UIVaultController : Controller
{
private readonly PaymentMethodHandlerDictionary _handlers;
private readonly IAuthorizationService _authorizationService;
public UIVaultController(PaymentMethodHandlerDictionary handlers, IAuthorizationService authorizationService)
{
_handlers = handlers;
_authorizationService = authorizationService;
}
[Route("{cryptoCode}/xpub")]
[Route("wallets/{walletId}/xpub")]
public async Task<IActionResult> VaultBridgeConnection(string cryptoCode = null,
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId = null)
{
if (!HttpContext.WebSockets.IsWebSocketRequest)
return NotFound();
cryptoCode = cryptoCode ?? walletId.CryptoCode;
bool versionChecked = false;
using (var cts = new CancellationTokenSource(TimeSpan.FromMinutes(10)))
{
var cancellationToken = cts.Token;
if (!_handlers.TryGetValue(PaymentTypes.CHAIN.GetPaymentMethodId(cryptoCode), out var h) || h is not IHasNetwork { Network: var network })
return NotFound();
var websocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
var vaultClient = new VaultClient(websocket);
var hwi = new Hwi.HwiClient(network.NBitcoinNetwork)
{
Transport = new VaultHWITransport(vaultClient)
};
Hwi.HwiDeviceClient device = null;
HwiEnumerateEntry deviceEntry = null;
HDFingerprint? fingerprint = null;
string password = null;
var websocketHelper = new WebSocketHelper(websocket);
async Task FetchFingerprint()
{
fingerprint = (await device.GetXPubAsync(new KeyPath("44'"), cancellationToken)).ExtPubKey.ParentFingerprint;
device = new HwiDeviceClient(hwi, DeviceSelectors.FromFingerprint(fingerprint.Value), deviceEntry.Model, fingerprint) { Password = password };
}
async Task<bool> RequireDeviceUnlocking()
{
if (deviceEntry == null)
{
await websocketHelper.Send("{ \"error\": \"need-device\"}", cancellationToken);
return true;
}
if (deviceEntry.Code is HwiErrorCode.DeviceNotInitialized)
{
await websocketHelper.Send("{ \"error\": \"need-initialized\"}", cancellationToken);
return true;
}
if (deviceEntry.Code is HwiErrorCode.DeviceNotReady)
{
if (IsTrezorT(deviceEntry))
{
await websocketHelper.Send("{ \"error\": \"need-passphrase-on-device\"}", cancellationToken);
return true;
}
else if (deviceEntry.NeedsPinSent is true)
{
await websocketHelper.Send("{ \"error\": \"need-pin\"}", cancellationToken);
return true;
}
else if (deviceEntry.NeedsPassphraseSent is true && password is null)
{
await websocketHelper.Send("{ \"error\": \"need-passphrase\"}", cancellationToken);
return true;
}
}
if (IsTrezorOne(deviceEntry) && password is null)
{
fingerprint = null; // There will be a new fingerprint
device = new HwiDeviceClient(hwi, DeviceSelectors.FromDeviceType("trezor", deviceEntry.Path), deviceEntry.Model, null);
await websocketHelper.Send("{ \"error\": \"need-passphrase\"}", cancellationToken);
return true;
}
return false;
}
JObject o = null;
try
{
while (true)
{
var command = await websocketHelper.NextMessageAsync(cancellationToken);
switch (command)
{
case "set-passphrase":
device.Password = await websocketHelper.NextMessageAsync(cancellationToken);
password = device.Password;
break;
case "ask-sign":
if (await RequireDeviceUnlocking())
{
continue;
}
if (walletId == null)
{
await websocketHelper.Send("{ \"error\": \"invalid-walletId\"}", cancellationToken);
continue;
}
if (fingerprint is null)
{
await FetchFingerprint();
}
await websocketHelper.Send("{ \"info\": \"ready\"}", cancellationToken);
o = JObject.Parse(await websocketHelper.NextMessageAsync(cancellationToken));
var authorization = await _authorizationService.AuthorizeAsync(User, Policies.CanModifyStoreSettings);
if (!authorization.Succeeded)
{
await websocketHelper.Send("{ \"error\": \"not-authorized\"}", cancellationToken);
continue;
}
var psbt = PSBT.Parse(o["psbt"].Value<string>(), network.NBitcoinNetwork);
var derivationSettings = GetDerivationSchemeSettings(walletId);
derivationSettings.RebaseKeyPaths(psbt);
// we ensure that the device fingerprint is part of the derivation settings
if (derivationSettings.AccountKeySettings.All(a => a.RootFingerprint != fingerprint))
{
await websocketHelper.Send("{ \"error\": \"wrong-wallet\"}", cancellationToken);
continue;
}
// otherwise, let the device check if it can sign anything
var signableInputs = psbt.Inputs
.SelectMany(i => i.HDKeyPaths)
.Where(i => i.Value.MasterFingerprint == fingerprint)
.ToArray();
if (signableInputs.Length > 0)
{
var actualPubKey = (await device.GetXPubAsync(signableInputs[0].Value.KeyPath)).GetPublicKey();
if (actualPubKey != signableInputs[0].Key)
{
await websocketHelper.Send("{ \"error\": \"wrong-keypath\"}", cancellationToken);
continue;
}
if (derivationSettings.IsMultiSigOnServer)
{
var alreadySigned = psbt.Inputs.Any(a =>
a.PartialSigs.Any(a => a.Key == actualPubKey));
if (alreadySigned)
{
await websocketHelper.Send("{ \"error\": \"already-signed-psbt\"}", cancellationToken);
continue;
}
}
}
try
{
psbt = await device.SignPSBTAsync(psbt, cancellationToken);
}
catch (HwiException)
{
await websocketHelper.Send("{ \"error\": \"user-reject\"}", cancellationToken);
continue;
}
o = new JObject();
o.Add("psbt", psbt.ToBase64());
await websocketHelper.Send(o.ToString(), cancellationToken);
break;
case "display-address":
if (await RequireDeviceUnlocking())
{
continue;
}
var k = RootedKeyPath.Parse(await websocketHelper.NextMessageAsync(cancellationToken));
await device.DisplayAddressAsync(GetScriptPubKeyType(k), k.KeyPath, cancellationToken);
await websocketHelper.Send("{ \"info\": \"ok\"}", cancellationToken);
break;
case "ask-pin":
if (device == null)
{
await websocketHelper.Send("{ \"error\": \"need-device\"}", cancellationToken);
continue;
}
try
{
await device.PromptPinAsync(cancellationToken);
}
catch (HwiException ex) when (ex.ErrorCode == HwiErrorCode.DeviceAlreadyUnlocked)
{
await websocketHelper.Send("{ \"error\": \"device-already-unlocked\"}", cancellationToken);
continue;
}
await websocketHelper.Send("{ \"info\": \"prompted, please input the pin\"}", cancellationToken);
var pin = int.Parse(await websocketHelper.NextMessageAsync(cancellationToken), CultureInfo.InvariantCulture);
if (await device.SendPinAsync(pin, cancellationToken))
{
goto askdevice;
}
else
{
await websocketHelper.Send("{ \"error\": \"incorrect-pin\"}", cancellationToken);
continue;
}
case "ask-xpub":
if (await RequireDeviceUnlocking())
{
continue;
}
await websocketHelper.Send("{ \"info\": \"ok\"}", cancellationToken);
var askedXpub = JObject.Parse(await websocketHelper.NextMessageAsync(cancellationToken));
var addressType = askedXpub["addressType"].Value<string>();
var accountNumber = askedXpub["accountNumber"].Value<int>();
JObject result = new JObject();
var factory = network.NBXplorerNetwork.DerivationStrategyFactory;
if (fingerprint is null)
{
await FetchFingerprint();
}
result["fingerprint"] = fingerprint.Value.ToString();
DerivationStrategyBase strategy = null;
KeyPath keyPath = (addressType switch
{
"taproot" => new KeyPath("86'"),
"segwit" => new KeyPath("84'"),
"segwitWrapped" => new KeyPath("49'"),
"legacy" => new KeyPath("44'"),
_ => null
})?.Derive(network.CoinType).Derive(accountNumber, true);
if (keyPath is null)
{
await websocketHelper.Send("{ \"error\": \"invalid-addresstype\"}", cancellationToken);
continue;
}
BitcoinExtPubKey xpub = await device.GetXPubAsync(keyPath);
if (!network.NBitcoinNetwork.Consensus.SupportSegwit && addressType != "legacy")
{
await websocketHelper.Send("{ \"error\": \"segwit-notsupported\"}", cancellationToken);
continue;
}
if (!network.NBitcoinNetwork.Consensus.SupportTaproot && addressType == "taproot")
{
await websocketHelper.Send("{ \"error\": \"taproot-notsupported\"}", cancellationToken);
continue;
}
if (addressType == "taproot")
{
strategy = factory.CreateDirectDerivationStrategy(xpub, new DerivationStrategyOptions()
{
ScriptPubKeyType = ScriptPubKeyType.TaprootBIP86
});
}
else if (addressType == "segwit")
{
strategy = factory.CreateDirectDerivationStrategy(xpub, new DerivationStrategyOptions()
{
ScriptPubKeyType = ScriptPubKeyType.Segwit
});
}
else if (addressType == "segwitWrapped")
{
strategy = factory.CreateDirectDerivationStrategy(xpub, new DerivationStrategyOptions()
{
ScriptPubKeyType = ScriptPubKeyType.SegwitP2SH
});
}
else if (addressType == "legacy")
{
strategy = factory.CreateDirectDerivationStrategy(xpub, new DerivationStrategyOptions()
{
ScriptPubKeyType = ScriptPubKeyType.Legacy
});
}
result.Add(new JProperty("strategy", strategy.ToString()));
result.Add(new JProperty("accountKey", xpub.ToString()));
result.Add(new JProperty("keyPath", keyPath.ToString()));
await websocketHelper.Send(result.ToString(), cancellationToken);
break;
case "ask-passphrase":
if (command == "ask-passphrase")
{
if (deviceEntry == null)
{
await websocketHelper.Send("{ \"error\": \"need-device\"}", cancellationToken);
continue;
}
// The make the trezor T ask for password
await device.GetXPubAsync(new KeyPath("44'"), cancellationToken);
}
goto askdevice;
case "ask-device":
askdevice:
if (!versionChecked)
{
var version = await hwi.GetVersionAsync(cancellationToken);
if (version.Major < 2)
{
await websocketHelper.Send("{ \"error\": \"vault-outdated\"}", cancellationToken);
continue;
}
versionChecked = true;
}
password = null;
deviceEntry = null;
device = null;
var entries = (await hwi.EnumerateEntriesAsync(cancellationToken)).ToList();
deviceEntry = entries.FirstOrDefault();
if (deviceEntry == null)
{
await websocketHelper.Send("{ \"error\": \"no-device\"}", cancellationToken);
continue;
}
var model = deviceEntry.Model ?? "Unsupported hardware wallet, try to update BTCPay Server Vault";
device = new HwiDeviceClient(hwi, deviceEntry.DeviceSelector, model, deviceEntry.Fingerprint);
fingerprint = device.Fingerprint;
JObject json = new JObject();
json.Add("model", model);
await websocketHelper.Send(json.ToString(), cancellationToken);
break;
}
}
}
catch (FormatException ex)
{
JObject obj = new JObject();
obj.Add("error", "invalid-network");
obj.Add("details", ex.ToString());
try
{
await websocketHelper.Send(obj.ToString(), cancellationToken);
}
catch { }
}
catch (Exception ex)
{
JObject obj = new JObject();
obj.Add("error", "unknown-error");
obj.Add("message", ex.Message);
obj.Add("details", ex.ToString());
try
{
await websocketHelper.Send(obj.ToString(), cancellationToken);
}
catch { }
}
finally
{
await websocketHelper.DisposeAsync(cancellationToken);
}
}
return new EmptyResult();
}
private ScriptPubKeyType GetScriptPubKeyType(RootedKeyPath keyPath)
{
var path = keyPath.KeyPath.ToString();
if (path.StartsWith("86'", StringComparison.OrdinalIgnoreCase))
return ScriptPubKeyType.TaprootBIP86;
if (path.StartsWith("84'", StringComparison.OrdinalIgnoreCase))
return ScriptPubKeyType.Segwit;
if (path.StartsWith("49'", StringComparison.OrdinalIgnoreCase))
return ScriptPubKeyType.SegwitP2SH;
if (path.StartsWith("44'", StringComparison.OrdinalIgnoreCase))
return ScriptPubKeyType.Legacy;
throw new NotSupportedException("Unsupported keypath");
}
private bool SameSelector(DeviceSelector a, DeviceSelector b)
{
var aargs = new List<string>();
a.AddArgs(aargs);
var bargs = new List<string>();
b.AddArgs(bargs);
if (aargs.Count != bargs.Count)
return false;
for (int i = 0; i < aargs.Count; i++)
{
if (aargs[i] != bargs[i])
return false;
}
return true;
}
private static bool IsTrezorT(HwiEnumerateEntry deviceEntry)
{
return deviceEntry.Model.Contains("Trezor_T", StringComparison.OrdinalIgnoreCase);
}
private static bool IsTrezorOne(HwiEnumerateEntry deviceEntry)
{
return deviceEntry.Model.Contains("trezor_1", StringComparison.OrdinalIgnoreCase);
}
public StoreData CurrentStore
{
get
{
return HttpContext.GetStoreData();
}
}
private DerivationSchemeSettings GetDerivationSchemeSettings(WalletId walletId)
{
var pmi = Payments.PaymentTypes.CHAIN.GetPaymentMethodId(walletId.CryptoCode);
return CurrentStore.GetPaymentMethodConfig<DerivationSchemeSettings>(pmi, _handlers);
}
}
}

View File

@@ -1324,7 +1324,7 @@ namespace BTCPayServer.Controllers
vm.Outputs.Last().Labels = vm.Outputs.Last().Labels.Concat(addressLabels.Select(tuple => tuple.Label)).ToArray(); vm.Outputs.Last().Labels = vm.Outputs.Last().Labels.Concat(addressLabels.Select(tuple => tuple.Label)).ToArray();
} }
} }
private IActionResult ViewVault(WalletId walletId, WalletPSBTViewModel vm) private IActionResult ViewVault(WalletId walletId, WalletPSBTViewModel vm)
{ {
return View(nameof(WalletSendVault), return View(nameof(WalletSendVault),
@@ -1332,8 +1332,6 @@ namespace BTCPayServer.Controllers
{ {
SigningContext = vm.SigningContext, SigningContext = vm.SigningContext,
WalletId = walletId.ToString(), WalletId = walletId.ToString(),
WebsocketPath = Url.Action(nameof(UIVaultController.VaultBridgeConnection), "UIVault",
new { walletId = walletId.ToString() }),
ReturnUrl = vm.ReturnUrl, ReturnUrl = vm.ReturnUrl,
BackUrl = vm.BackUrl BackUrl = vm.BackUrl
}); });

View File

@@ -21,6 +21,7 @@ using BTCPayServer.BIP78.Sender;
using BTCPayServer.Configuration; using BTCPayServer.Configuration;
using BTCPayServer.Data; using BTCPayServer.Data;
using BTCPayServer.HostedServices; using BTCPayServer.HostedServices;
using BTCPayServer.Hwi;
using BTCPayServer.Lightning; using BTCPayServer.Lightning;
using BTCPayServer.Models; using BTCPayServer.Models;
using BTCPayServer.Models.StoreViewModels; using BTCPayServer.Models.StoreViewModels;
@@ -34,6 +35,7 @@ using BTCPayServer.Services;
using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Reporting; using BTCPayServer.Services.Reporting;
using BTCPayServer.Services.Wallets; using BTCPayServer.Services.Wallets;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@@ -52,6 +54,39 @@ namespace BTCPayServer
{ {
public static class Extensions public static class Extensions
{ {
public static string GetNiceModelName(this HwiDeviceClient device)
=> device.Model switch
{
"trezor_1" => "Trezor Model One",
"trezor_t" => "Trezor Model T",
"trezor_r" => "Trezor Model R",
"coldcard" => "Coldcard",
"coldcard_simulator" => "Coldcard (Simulator)",
"trezor_safe 3" => "Trezor Safe 3",
"trezor_safe 5" => "Trezor Safe 5",
"ledger_nano_s" => "Ledger Nano S",
"ledger_nano_x" => "Ledger Nano X",
"ledger_stax" => "Ledger Stax",
"ledger_flex" => "Ledger Flex",
"keepkey" => "KeepKey",
"digitalbitbox_01" or "digitalbitbox" => "Digital Bitbox",
"digitalbitbox_01_simulator" => "Digital Bitbox (Simulator)",
"bitbox02_multi" => "BitBox02 Multi",
"bitbox02_btconly" => "BitBox02 Bitcoin Only",
"ledger_nano_s_plus" => "Ledger Nano S Plus",
"jade" => "Jade",
_ => device.Model
};
public static string ProtectString(this IDataProtector protector,string str)
{
return Convert.ToBase64String(protector.Protect(Encoding.UTF8.GetBytes(str)));
}
public static string UnprotectString(this IDataProtector protector, string str)
{
return Encoding.UTF8.GetString(protector.Unprotect(Convert.FromBase64String(str)));
}
/// <summary> /// <summary>
/// Outputs a serializer which will serialize default and null members. /// Outputs a serializer which will serialize default and null members.
/// This is useful for discovering the API. /// This is useful for discovering the API.

View File

@@ -3,7 +3,8 @@ namespace BTCPayServer.Models
public class SetupBoltcardViewModel public class SetupBoltcardViewModel
{ {
public string ReturnUrl { get; set; } public string ReturnUrl { get; set; }
public string WebsocketPath { get; set; } public string BoltcardUrl { get; set; }
public string Command { get; set; } public bool NewCard { get; set; }
public string PullPaymentId { get; set; }
} }
} }

View File

@@ -15,7 +15,6 @@ namespace BTCPayServer.Models.StoreViewModels
{ {
get; set; get; set;
} = new List<(string KeyPath, string Address, RootedKeyPath RootedKeyPath)>(); } = new List<(string KeyPath, string Address, RootedKeyPath RootedKeyPath)>();
public string CryptoCode { get; set; } public string CryptoCode { get; set; }
public string KeyPath { get; set; } public string KeyPath { get; set; }
[Display(Name = "Root fingerprint")] [Display(Name = "Root fingerprint")]
@@ -28,8 +27,6 @@ namespace BTCPayServer.Models.StoreViewModels
public string WalletFileContent { get; set; } public string WalletFileContent { get; set; }
public string Config { get; set; } public string Config { get; set; }
public string Source { get; set; } public string Source { get; set; }
[Display(Name = "Derivation scheme format")]
public string DerivationSchemeFormat { get; set; }
[Display(Name = "Account key")] [Display(Name = "Account key")]
public string AccountKey { get; set; } public string AccountKey { get; set; }
public BTCPayNetwork Network { get; set; } public BTCPayNetwork Network { get; set; }

View File

@@ -3,7 +3,6 @@ namespace BTCPayServer.Models.WalletViewModels
public class WalletSendVaultModel : IHasBackAndReturnUrl public class WalletSendVaultModel : IHasBackAndReturnUrl
{ {
public string WalletId { get; set; } public string WalletId { get; set; }
public string WebsocketPath { get; set; }
public string BackUrl { get; set; } public string BackUrl { get; set; }
public string ReturnUrl { get; set; } public string ReturnUrl { get; set; }
public SigningContextModel SigningContext { get; set; } = new(); public SigningContextModel SigningContext { get; set; } = new();

View File

@@ -83,6 +83,7 @@ namespace BTCPayServer.Services
"Account key path": "", "Account key path": "",
"Account successfully created.": "", "Account successfully created.": "",
"Account successfully deleted.": "", "Account successfully deleted.": "",
"Action canceled by user": "",
"Actions": "", "Actions": "",
"Active": "", "Active": "",
"Add": "", "Add": "",
@@ -108,7 +109,7 @@ namespace BTCPayServer.Services
"Additional text to provide an explanation for the field": "", "Additional text to provide an explanation for the field": "",
"Address": "", "Address": "",
"Address type": "", "Address type": "",
"Address verification": "", "Address verified.": "",
"Adjust the design of your BTCPay Server instance to your needs.": "", "Adjust the design of your BTCPay Server instance to your needs.": "",
"Adjusts the generated invoice amount — use as a prefix to have multiple adjustment fields": "", "Adjusts the generated invoice amount — use as a prefix to have multiple adjustment fields": "",
"Adjusts the generated invoice amount by multiplying with this value — use as a prefix to have multiple adjustment fields": "", "Adjusts the generated invoice amount by multiplying with this value — use as a prefix to have multiple adjustment fields": "",
@@ -146,6 +147,7 @@ namespace BTCPayServer.Services
"An invitation email has been sent.<br/>You may alternatively share this link with them: <a class='alert-link' href='{0}'>{0}</a>": "", "An invitation email has been sent.<br/>You may alternatively share this link with them: <a class='alert-link' href='{0}'>{0}</a>": "",
"An invitation email has not been sent, because the server does not have an email server configured.<br/> You need to share this link with them: <a class='alert-link' href='{0}'>{0}</a>": "", "An invitation email has not been sent, because the server does not have an email server configured.<br/> You need to share this link with them: <a class='alert-link' href='{0}'>{0}</a>": "",
"An invoice must be paid within a defined time interval at a fixed exchange rate to protect the issuer from price fluctuations.": "", "An invoice must be paid within a defined time interval at a fixed exchange rate to protect the issuer from price fluctuations.": "",
"An unexpected error happened: {0}": "",
"Animation": "", "Animation": "",
"Any amount": "", "Any amount": "",
"Any uploaded files are being saved on the same machine that hosts BTCPay; please pay attention to your storage space.": "", "Any uploaded files are being saved on the same machine that hosts BTCPay; please pay attention to your storage space.": "",
@@ -208,6 +210,7 @@ namespace BTCPayServer.Services
"Boltcard is configured": "", "Boltcard is configured": "",
"Brand Color": "", "Brand Color": "",
"Branding": "", "Branding": "",
"Brave supports BTCPay Server Vault, but you need to disable Brave Shields. (<a class=\"alert-link\" href=\"https://www.updateland.com/how-to-turn-off-brave-shields/\" target=\"_blank\" rel=\"noreferrer noopener\">More information</a>)": "",
"Broadcast (Payjoin)": "", "Broadcast (Payjoin)": "",
"Broadcast (Simple)": "", "Broadcast (Simple)": "",
"Broadcast transaction": "", "Broadcast transaction": "",
@@ -218,9 +221,8 @@ namespace BTCPayServer.Services
"BTCPay Server Configurator": "", "BTCPay Server Configurator": "",
"BTCPay Server currently supports:": "", "BTCPay Server currently supports:": "",
"BTCPay Server Supporters": "", "BTCPay Server Supporters": "",
"BTCPay Server Vault does not seem to be running, you can download it on {0}.": "", "BTCPay Server Vault does not seem to be running, you can download it on <a target=\"_blank\" href=\"https://github.com/btcpayserver/BTCPayServer.Vault/releases/latest\">Github</a>.": "",
"BTCPay will restart momentarily.": "", "BTCPay will restart momentarily.": "",
"BTCPayServer successfully connected to the vault.": "",
"Bump fee": "", "Bump fee": "",
"But now, what if you want to support <code>DOGE</code>? The problem with <code>DOGE</code> is that most exchange do not have any pair for it. But <code>bitpay</code> has a <code>DOGE_BTC</code> pair.<br />Luckily, the rule engine allow you to reference rules:": "", "But now, what if you want to support <code>DOGE</code>? The problem with <code>DOGE</code> is that most exchange do not have any pair for it. But <code>bitpay</code> has a <code>DOGE_BTC</code> pair.<br />Luckily, the rule engine allow you to reference rules:": "",
"Button Type": "", "Button Type": "",
@@ -254,6 +256,8 @@ namespace BTCPayServer.Services
"Cheat Mode: Send funds to this wallet": "", "Cheat Mode: Send funds to this wallet": "",
"Check if NFC is supported and enabled on this device": "", "Check if NFC is supported and enabled on this device": "",
"Check releases on GitHub and notify when new BTCPay Server version is available": "", "Check releases on GitHub and notify when new BTCPay Server version is available": "",
"Checking BTCPay Server Vault is running...": "",
"Checking if this device can sign the transaction...": "",
"Checkout": "", "Checkout": "",
"Checkout Additional Query String": "", "Checkout Additional Query String": "",
"Checkout Appearance": "", "Checkout Appearance": "",
@@ -302,7 +306,6 @@ namespace BTCPayServer.Services
"Confirm new password": "", "Confirm new password": "",
"Confirm passphrase": "", "Confirm passphrase": "",
"Confirm password": "", "Confirm password": "",
"Confirm that you see the following address on the device:": "",
"Confirmations": "", "Confirmations": "",
"confirmed": "", "confirmed": "",
"Connect an existing wallet": "", "Connect an existing wallet": "",
@@ -433,7 +436,6 @@ namespace BTCPayServer.Services
"Dependencies": "", "Dependencies": "",
"Dependencies not met.": "", "Dependencies not met.": "",
"Derivation scheme": "", "Derivation scheme": "",
"Derivation scheme format": "",
"Description": "", "Description": "",
"Description template of the lightning invoice": "", "Description template of the lightning invoice": "",
"Destination": "", "Destination": "",
@@ -442,6 +444,7 @@ namespace BTCPayServer.Services
"Detects the language of the customer's browser.": "", "Detects the language of the customer's browser.": "",
"Determine the generated invoice amount": "", "Determine the generated invoice amount": "",
"Determine the generated invoice currency": "", "Determine the generated invoice currency": "",
"Device found: {0}": "",
"Dictionaries": "", "Dictionaries": "",
"Dictionaries enable you to translate the BTCPay Server backend into different languages.": "", "Dictionaries enable you to translate the BTCPay Server backend into different languages.": "",
"Dictionary": "", "Dictionary": "",
@@ -558,6 +561,8 @@ namespace BTCPayServer.Services
"Enter destination to claim funds": "", "Enter destination to claim funds": "",
"Enter extended public key": "", "Enter extended public key": "",
"Enter the code in the confirmation box below.": "", "Enter the code in the confirmation box below.": "",
"Enter the passphrase.": "",
"Enter the pin.": "",
"Enter the wallet seed": "", "Enter the wallet seed": "",
"Enter wallet seed": "", "Enter wallet seed": "",
"Enter your extended public key": "", "Enter your extended public key": "",
@@ -591,6 +596,9 @@ namespace BTCPayServer.Services
"Fee rate": "", "Fee rate": "",
"Fee rate (sat/vB)": "", "Fee rate (sat/vB)": "",
"Fee will be shown for BTC and LTC onchain payments only.": "", "Fee will be shown for BTC and LTC onchain payments only.": "",
"Fetching device...": "",
"Fetching public keys...": "",
"Fetching wallet's fingerprint.": "",
"FIDO2 Authentication": "", "FIDO2 Authentication": "",
"Field to mirror": "", "Field to mirror": "",
"File Id": "", "File Id": "",
@@ -707,6 +715,7 @@ namespace BTCPayServer.Services
"In use": "", "In use": "",
"In-store": "", "In-store": "",
"Include archived": "", "Include archived": "",
"Incorrect pin code.": "",
"Increase the security of your instance by disabling the ability to change the SSH settings in this BTCPay Server instance's user interface.": "", "Increase the security of your instance by disabling the ability to change the SSH settings in this BTCPay Server instance's user interface.": "",
"Index": "", "Index": "",
"Input the key string manually": "", "Input the key string manually": "",
@@ -726,6 +735,7 @@ namespace BTCPayServer.Services
"Invalid login attempt.": "", "Invalid login attempt.": "",
"Invalid network": "", "Invalid network": "",
"Invalid passphrase confirmation": "", "Invalid passphrase confirmation": "",
"Invalid password confirmation.": "",
"Invalid payout method": "", "Invalid payout method": "",
"Invalid PSBT": "", "Invalid PSBT": "",
"Invalid role": "", "Invalid role": "",
@@ -875,6 +885,7 @@ namespace BTCPayServer.Services
"No contributions allowed after the goal has been reached": "", "No contributions allowed after the goal has been reached": "",
"No contributions have been made yet.": "", "No contributions have been made yet.": "",
"No deliveries for this webhook yet": "", "No deliveries for this webhook yet": "",
"No device connected.": "",
"No documentation": "", "No documentation": "",
"No end date has been set": "", "No end date has been set": "",
"No expiry date has been set for this payment request": "", "No expiry date has been set for this payment request": "",
@@ -959,6 +970,7 @@ namespace BTCPayServer.Services
"Passphrase confirmation": "", "Passphrase confirmation": "",
"Password": "", "Password": "",
"Password (leave blank to generate invite-link)": "", "Password (leave blank to generate invite-link)": "",
"Password entered...": "",
"Password Reset": "", "Password Reset": "",
"Password successfully set": "", "Password successfully set": "",
"Password successfully set.": "", "Password successfully set.": "",
@@ -1014,6 +1026,7 @@ namespace BTCPayServer.Services
"Percentage must be a numeric value between 0 and 100": "", "Percentage must be a numeric value between 0 and 100": "",
"Permanent Url": "", "Permanent Url": "",
"Permissions": "", "Permissions": "",
"Pin code verified.": "",
"Placeholder": "", "Placeholder": "",
"Placeholders": "", "Placeholders": "",
"Please check that your wallet is generating the same addresses as below.": "", "Please check that your wallet is generating the same addresses as below.": "",
@@ -1034,9 +1047,13 @@ namespace BTCPayServer.Services
"Please provide your existing seed": "", "Please provide your existing seed": "",
"Please provide your extended public key": "", "Please provide your extended public key": "",
"Please remove the NFC from the card reader": "", "Please remove the NFC from the card reader": "",
"Please review and confirm the transaction on your device...": "",
"Please select an option before proceeding": "", "Please select an option before proceeding": "",
"Please set NBXPlorer's PostgreSQL connection string to make this feature available.": "", "Please set NBXPlorer's PostgreSQL connection string to make this feature available.": "",
"Please verify that the address displayed on your device is <b>{0}</b>...": "",
"Please wait for your node to be synched": "", "Please wait for your node to be synched": "",
"Please, confirm on the device first...": "",
"Please, enter the passphrase on the device.": "",
"Plugin action cancelled.": "", "Plugin action cancelled.": "",
"Plugin scheduled to be installed.": "", "Plugin scheduled to be installed.": "",
"Plugin scheduled to be uninstalled.": "", "Plugin scheduled to be uninstalled.": "",
@@ -1075,7 +1092,7 @@ namespace BTCPayServer.Services
"PSBT to combine with…": "", "PSBT to combine with…": "",
"PSBT updated!": "", "PSBT updated!": "",
"Public Key": "", "Public Key": "",
"Public Key Information": "", "Public keys successfully fetched.": "",
"Public Node Info": "", "Public Node Info": "",
"Pull payment archived": "", "Pull payment archived": "",
"Pull payment request created": "", "Pull payment request created": "",
@@ -1191,12 +1208,14 @@ namespace BTCPayServer.Services
"Roles": "", "Roles": "",
"Root fingerprint": "", "Root fingerprint": "",
"RPC Error while broadcasting: {0}": "", "RPC Error while broadcasting: {0}": "",
"Safari doesn't support BTCPay Server Vault. Please use a different browser. (<a class=\"alert-link\" href=\"https://bugs.webkit.org/show_bug.cgi?id=171934\" target=\"_blank\" rel=\"noreferrer noopener\">More information</a>)": "",
"sale": "", "sale": "",
"sales": "", "sales": "",
"Sales": "", "Sales": "",
"Save": "", "Save": "",
"Save comment": "", "Save comment": "",
"Save Wallet Settings": "", "Save Wallet Settings": "",
"Saving...": "",
"Scan destination with camera": "", "Scan destination with camera": "",
"Scan Login code with camera": "", "Scan Login code with camera": "",
"Scan QR code": "", "Scan QR code": "",
@@ -1239,6 +1258,7 @@ namespace BTCPayServer.Services
"Select the payout method used for refund": "", "Select the payout method used for refund": "",
"Select the store to grant permission for": "", "Select the store to grant permission for": "",
"Select this contribution perk": "", "Select this contribution perk": "",
"Select your address type and account": "",
"selected": "", "selected": "",
"Send": "", "Send": "",
"Send {0}": "", "Send {0}": "",
@@ -1288,7 +1308,6 @@ namespace BTCPayServer.Services
"Show current info": "", "Show current info": "",
"Show export QR": "", "Show export QR": "",
"Show multi-sig examples": "", "Show multi-sig examples": "",
"Show on device": "",
"Show plugins in pre-release": "", "Show plugins in pre-release": "",
"Show QR": "", "Show QR": "",
"Show QR Code": "", "Show QR Code": "",
@@ -1368,8 +1387,8 @@ namespace BTCPayServer.Services
"Switch date format": "", "Switch date format": "",
"Syntax error": "", "Syntax error": "",
"System": "", "System": "",
"Taproot": "",
"Taproot (For advanced users)": "", "Taproot (For advanced users)": "",
"Taproot (ONLY FOR DEVELOPMENT)": "",
"Target Amount": "", "Target Amount": "",
"Template": "", "Template": "",
"Template JSON": "", "Template JSON": "",
@@ -1411,6 +1430,7 @@ namespace BTCPayServer.Services
"The crypto currency price, at the rate the invoice got paid.": "", "The crypto currency price, at the rate the invoice got paid.": "",
"The currency to generate the invoice in when generated through this lightning address": "", "The currency to generate the invoice in when generated through this lightning address": "",
"The custom color, logo and CSS are applied on the public/customer-facing pages (Invoice, Payment Request, Pull Payment, etc.). The brand color is used as the accent color for buttons, links, etc. It might get adapted to fit the light/dark color scheme.": "", "The custom color, logo and CSS are applied on the public/customer-facing pages (Invoice, Payment Request, Pull Payment, etc.). The brand color is used as the accent color for buttons, links, etc. It might get adapted to fit the light/dark color scheme.": "",
"The device has not been initialized.": "",
"The DNS record has been refreshed:": "", "The DNS record has been refreshed:": "",
"The Dynamic DNS has been successfully queried, your configuration is saved": "", "The Dynamic DNS has been successfully queried, your configuration is saved": "",
"The Dynamic DNS service has been disabled": "", "The Dynamic DNS service has been disabled": "",
@@ -1515,6 +1535,9 @@ namespace BTCPayServer.Services
"This card is already in a factory state": "", "This card is already in a factory state": "",
"This card is already properly configured": "", "This card is already properly configured": "",
"This crowdfund page is not publicly viewable!": "", "This crowdfund page is not publicly viewable!": "",
"This device already signed PSBT.": "",
"This device can't sign the transaction. (The wallet keypath in your wallet settings seems incorrect)": "",
"This device can't sign the transaction. (Wrong device, wrong passphrase or wrong device fingerprint in your wallet settings)": "",
"This feature is disabled": "", "This feature is disabled": "",
"This feature is only available to BTC wallets": "", "This feature is only available to BTC wallets": "",
"This full node does not support rescan of the UTXO set": "", "This full node does not support rescan of the UTXO set": "",
@@ -1582,6 +1605,7 @@ namespace BTCPayServer.Services
"Transaction Details": "", "Transaction Details": "",
"Transaction fee rate:": "", "Transaction fee rate:": "",
"Transaction Id": "", "Transaction Id": "",
"Transaction signed successfully, proceeding to review...": "",
"transactions": "", "transactions": "",
"Translations": "", "Translations": "",
"Translations are formatted as JSON; for example, <b>{0}</b> translates <b>{1}</b> to <b>{2}</b>.": "", "Translations are formatted as JSON; for example, <b>{0}</b> translates <b>{1}</b> to <b>{2}</b>.": "",
@@ -1596,7 +1620,7 @@ namespace BTCPayServer.Services
"Unarchive this payment request": "", "Unarchive this payment request": "",
"Unarchive this store": "", "Unarchive this store": "",
"Unavailable": "", "Unavailable": "",
"Unexpected error: {0}": "", "Unexpected address returned by the device...": "",
"Unexpected payjoin error: {0}": "", "Unexpected payjoin error: {0}": "",
"Unify on-chain and lightning payment URL/QR code": "", "Unify on-chain and lightning payment URL/QR code": "",
"Uninstall": "", "Uninstall": "",
@@ -1608,6 +1632,7 @@ namespace BTCPayServer.Services
"Unnamed Store": "", "Unnamed Store": "",
"Unread": "", "Unread": "",
"Unsupported exchange": "", "Unsupported exchange": "",
"Unsupported hardware wallet, try to update BTCPay Server Vault": "",
"Unusual": "", "Unusual": "",
"Update": "", "Update": "",
"Update Crowdfund": "", "Update Crowdfund": "",
@@ -1681,6 +1706,7 @@ namespace BTCPayServer.Services
"Verification email sent. Please check your email.": "", "Verification email sent. Please check your email.": "",
"Verify": "", "Verify": "",
"Verify that the <code>orderId</code> is from your backend, that the <code>price</code> is correct and that <code>status</code> is <code>settled</code>": "", "Verify that the <code>orderId</code> is from your backend, that the <code>price</code> is correct and that <code>status</code> is <code>settled</code>": "",
"Verifying pin...": "",
"via HTTPS": "", "via HTTPS": "",
"via TCP or unix domain socket connection": "", "via TCP or unix domain socket connection": "",
"via the REST API": "", "via the REST API": "",
@@ -1696,6 +1722,7 @@ namespace BTCPayServer.Services
"Wallet file content": "", "Wallet file content": "",
"Wallet Recovery Seed": "", "Wallet Recovery Seed": "",
"Wallet settings for {0} have been updated.": "", "Wallet settings for {0} have been updated.": "",
"Wallet's fingerprint fetched.": "",
"Wallet's private key is erased from the server. Higher security. To spend, you have to manually input the private key or import it into an external wallet.": "", "Wallet's private key is erased from the server. Higher security. To spend, you have to manually input the private key or import it into an external wallet.": "",
"Wallet's private key is stored on the server. Spending the funds you received is convenient. To minimize the risk of theft, regularly withdraw funds to a different wallet.": "", "Wallet's private key is stored on the server. Spending the funds you received is convenient. To minimize the risk of theft, regularly withdraw funds to a different wallet.": "",
"Wallets": "", "Wallets": "",
@@ -1763,6 +1790,7 @@ namespace BTCPayServer.Services
"Your account will no longer have this Lightning wallet as an option for two-factor authentication.": "", "Your account will no longer have this Lightning wallet as an option for two-factor authentication.": "",
"Your account will no longer have this security device as an option for two-factor authentication.": "", "Your account will no longer have this security device as an option for two-factor authentication.": "",
"Your available balance is": "", "Your available balance is": "",
"Your BTCPay Server Vault version is outdated. Please <a target=\"_blank\" href=\"https://github.com/btcpayserver/BTCPayServer.Vault/releases/latest\">download</a> the latest version.": "",
"Your dynamic DNS hostname": "", "Your dynamic DNS hostname": "",
"Your email has been confirmed.": "", "Your email has been confirmed.": "",
"Your email has been confirmed. Please set your password.": "", "Your email has been confirmed. Please set your password.": "",

View File

@@ -1,128 +1,56 @@
#nullable enable #nullable enable
using System; using System;
using System.Net.WebSockets; using System.Linq;
using System.Runtime.CompilerServices; using System.Text.Json.Nodes;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Amazon.S3.Model.Internal.MarshallTransformations; using Microsoft.JSInterop;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
namespace BTCPayServer namespace BTCPayServer;
public class VaultClient(IJSRuntime js, string serviceUri)
{ {
public enum VaultMessageType public class VaultNotConnectedException() : Exception("Vault not connected");
public class VaultException(string message) : Exception(message);
public async Task<VaultPermissionResult> AskPermission(CancellationToken cancellationToken)
{ {
Ok, return await js.InvokeAsync<VaultPermissionResult>("vault.askVaultPermission", cancellationToken, serviceUri);
Error,
Processing
} }
public enum VaultServices public async Task<JToken?> SendVaultRequest(string? path, JObject? body, CancellationToken cancellationToken)
{ {
HWI, var isAbsolute = path is not null && Uri.IsWellFormedUriString(path, UriKind.Absolute);
NFC var query = new JsonObject()
{
["uri"] = isAbsolute ? path : serviceUri + path
};
if (body is not null)
query["body"] = JsonObject.Parse(body.ToString());
var resp = await js.InvokeAsync<SendRequestResponse>("vault.sendRequest", cancellationToken, query);
if (resp.HttpCode is not { } p)
throw new VaultNotConnectedException();
if (p != 200)
throw new VaultException($"Unexpected response code from vault {p}");
return (resp.Body)?.ToJsonString() is { } str ? JToken.Parse(str) : null;
} }
public class VaultNotConnectedException : VaultException public class SendRequestResponse
{ {
public VaultNotConnectedException() : base("BTCPay Vault isn't connected") public int? HttpCode { get; set; }
{ public JsonNode? Body { get; set; }
}
} }
public class VaultException : Exception public class HwiResponse
{ {
public VaultException(string message) : base(message) public int HttpCode { get; set; }
{ public string? Error { get; set; }
public JsonNode? Body { get; set; }
}
}
public class VaultClient
{
public VaultClient(WebSocket websocket)
{
Websocket = new WebSocketHelper(websocket);
}
public WebSocketHelper Websocket { get; }
public async Task<string> GetNextCommand(CancellationToken cancellationToken)
{
return await Websocket.NextMessageAsync(cancellationToken);
}
public async Task SendMessage(JObject mess, CancellationToken cancellationToken)
{
await Websocket.Send(mess.ToString(), cancellationToken);
}
public Task Show(VaultMessageType type, string message, CancellationToken cancellationToken)
{
return Show(type, message, null, cancellationToken);
}
public async Task Show(VaultMessageType type, string message, string? debug, CancellationToken cancellationToken)
{
await SendMessage(new JObject()
{
["command"] = "showMessage",
["message"] = message,
["type"] = type.ToString(),
["debug"] = debug
}, cancellationToken);
}
string? _ServiceUri;
public async Task<bool?> AskPermission(VaultServices service, CancellationToken cancellationToken)
{
var uri = service switch
{
VaultServices.HWI => "http://127.0.0.1:65092/hwi-bridge/v1",
VaultServices.NFC => "http://127.0.0.1:65092/nfc-bridge/v1",
_ => throw new NotSupportedException()
};
await this.SendMessage(new JObject()
{
["command"] = "sendRequest",
["uri"] = uri + "/request-permission"
}, cancellationToken);
var result = await GetNextMessage(cancellationToken);
if (result["httpCode"] is { } p)
{
var ok = p.Value<int>() == 200;
if (ok)
_ServiceUri = uri;
return ok;
}
return null;
}
public async Task<JToken?> SendVaultRequest(string? path, JObject? body, CancellationToken cancellationToken)
{
var isAbsolute = path is not null && Uri.IsWellFormedUriString(path, UriKind.Absolute);
var query = new JObject()
{
["command"] = "sendRequest",
["uri"] = isAbsolute ? path : _ServiceUri + path
};
if (body is not null)
query["body"] = body;
await this.SendMessage(query, cancellationToken);
var resp = await GetNextMessage(cancellationToken);
if (resp["httpCode"] is not { } p)
throw new VaultNotConnectedException();
if (p.Value<int>() != 200)
throw new VaultException($"Unexpected response code from vault {p.Value<int>()}");
return resp["body"] as JToken;
}
public async Task<JObject> GetNextMessage(CancellationToken cancellationToken)
{
return JObject.Parse(await this.Websocket.NextMessageAsync(cancellationToken));
}
public Task SendSimpleMessage(string command, CancellationToken cancellationToken)
{
return SendMessage(new JObject() { ["command"] = command }, cancellationToken);
}
} }
} }
public class VaultPermissionResult
{
public int HttpCode { get; set; }
public string? Browser { get; set; }
}

View File

@@ -1,26 +1,22 @@
using System.Net.WebSockets; using System.Threading;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
namespace BTCPayServer namespace BTCPayServer;
{
public class VaultHWITransport : Hwi.Transports.ITransport
{
private readonly VaultClient _vaultClient;
public VaultHWITransport(VaultClient vaultClient) public class VaultHWITransport : Hwi.Transports.ITransport
{
private readonly VaultClient _client;
public VaultHWITransport(VaultClient client)
{
_client = client;
}
public async Task<string> SendCommandAsync(string[] arguments, CancellationToken cancellationToken)
{
return (await _client.SendVaultRequest(null, new JObject()
{ {
_vaultClient = vaultClient; ["params"] = new JArray(arguments)
} }, cancellationToken)).Value<string>() ?? "";
public async Task<string> SendCommandAsync(string[] arguments, CancellationToken cancel)
{
var resp = await _vaultClient.SendVaultRequest("http://127.0.0.1:65092/hwi-bridge/v1",
new JObject()
{
["params"] = new JArray(arguments)
}, cancel);
return (string)((JValue)resp).Value;
}
} }
} }

View File

@@ -1,22 +0,0 @@
<div id="walletAlert" class="alert alert-warning alert-dismissible my-4" style="display:none;" role="alert">
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="@StringLocalizer["Close"]">
<vc:icon symbol="close" />
</button>
<span id="alertMessage"></span>
</div>
<script>
var alertMsg = document.getElementById("alertMessage");
var walletAlert = document.getElementById("walletAlert");
var isSafari = window.safari !== undefined;
if (isSafari)
{
alertMsg.innerHTML = "Safari doesn't support BTCPay Server Vault. Please use a different browser. (<a class=\"alert-link\" href=\"https://bugs.webkit.org/show_bug.cgi?id=171934\" target=\"_blank\" rel=\"noreferrer noopener\">More information</a>)";
walletAlert.style.display = null;
}
var isBrave = navigator.brave !== undefined;
if (isBrave)
{
alertMsg.innerHTML = "Brave supports BTCPay Server Vault, but you need to disable Brave Shields. (<a class=\"alert-link\" href=\"https://www.updateland.com/how-to-turn-off-brave-shields/\" target=\"_blank\" rel=\"noreferrer noopener\">More information</a>)";
walletAlert.style.display = null;
}
</script>

View File

@@ -1,69 +0,0 @@
<template id="VaultConnection">
<div class="vault-feedback vault-feedback1 mb-2 d-flex align-items-center">
<vc:icon symbol="dots" css-class="vault-feedback-icon d-none"/>
<span class="vault-feedback-content flex-grow"></span>
</div>
<div class="vault-feedback vault-feedback2 mb-2 d-flex align-items-center">
<vc:icon symbol="dots" css-class="vault-feedback-icon d-none"/>
<span class="vault-feedback-content flex-grow"></span>
</div>
<div class="vault-feedback vault-feedback3 mb-2 d-flex align-items-center">
<vc:icon symbol="dots" css-class="vault-feedback-icon d-none"/>
<span class="vault-feedback-content flex-grow"></span>
</div>
<div class="vault-feedback vault-feedback4 mb-2 d-flex align-items-center">
<vc:icon symbol="dots" css-class="vault-feedback-icon d-none"/>
<span class="vault-feedback-content flex-grow"></span>
</div>
<div class="vault-feedback vault-feedback5 mb-2 d-flex align-items-center">
<vc:icon symbol="dots" css-class="vault-feedback-icon d-none"/>
<span class="vault-feedback-content flex-grow"></span>
</div>
<div id="pin-input" class="mt-4" style="display: none;">
<div class="row">
<div class="col">
<div class="input-group mb-2">
<input id="pin-display" type="text" class="form-control" readonly>
<div id="pin-display-delete" class="input-group-text cursor-pointer">
<vc:icon symbol="actions-remove" />
</div>
</div>
</div>
</div>
<div class="row">
<div class="col"><div class="pin-button" id="pin-7"></div></div>
<div class="col"><div class="pin-button" id="pin-8"></div></div>
<div class="col"><div class="pin-button" id="pin-9"></div></div>
</div>
<div class="row">
<div class="col"><div class="pin-button" id="pin-4"></div></div>
<div class="col"><div class="pin-button" id="pin-5"></div></div>
<div class="col"><div class="pin-button" id="pin-6"></div></div>
</div>
<div class="row">
<div class="col"><div class="pin-button" id="pin-1"></div></div>
<div class="col"><div class="pin-button" id="pin-2"></div></div>
<div class="col"><div class="pin-button" id="pin-3"></div></div>
</div>
</div>
<div id="passphrase-input" class="mt-4" style="display: none;">
<div class="form-group">
<label for="Password" class="form-label" text-translate="true">Passphrase (Leave empty if there isn't any passphrase)</label>
<div class="input-group">
<input id="Password" type="password" class="form-control">
<button type="button" class="btn btn-secondary px-3 only-for-js" title="@StringLocalizer["Toggle passphrase visibility"]" data-toggle-password="#Password">
<vc:icon symbol="actions-show" />
</button>
</div>
</div>
<div class="form-group">
<label for="PasswordConfirmation" class="form-label" text-translate="true">Passphrase confirmation</label>
<div class="input-group">
<input id="PasswordConfirmation" type="password" class="form-control">
<button type="button" class="btn btn-secondary px-3 only-for-js" title="@StringLocalizer["Toggle passphrase visibility"]" data-toggle-password="#PasswordConfirmation">
<vc:icon symbol="actions-show" />
</button>
</div>
</div>
</div>
</template>

View File

@@ -1,6 +1,5 @@
@{ @{
Layout = "_LayoutSimple"; Layout = "_LayoutSimple";
ViewData.SetBlazorAllowed(false);
} }
@section PageHeadContent { @section PageHeadContent {

View File

@@ -1,7 +1,12 @@
@using BTCPayServer.Controllers @using BTCPayServer.Abstractions.TagHelpers
@using BTCPayServer.Blazor
@using BTCPayServer.Blazor.VaultBridge
@using Microsoft.AspNetCore.Mvc.TagHelpers
@using BTCPayServer.TagHelpers
@model SetupBoltcardViewModel @model SetupBoltcardViewModel
@{ @{
Layout = "_LayoutWizard"; Layout = "_LayoutWizard";
this.ViewData.SetBlazorAllowed(true);
} }
@section Navbar { @section Navbar {
@@ -11,54 +16,44 @@
} }
<header class="text-center"> <header class="text-center">
<h1>@ViewData["Title"]</h1> @if (Model.NewCard)
{
<h1 text-translate="true">Setup Boltcard</h1>
}
else
{
<h1 text-translate="true">Reset Boltcard</h1>
}
<p class="lead text-secondary mt-3" text-translate="true">Using BTCPay Server Vault (NFC)</p> <p class="lead text-secondary mt-3" text-translate="true">Using BTCPay Server Vault (NFC)</p>
</header> </header>
<partial name="LocalhostBrowserSupport" /> <div class="row mt-5 mb-4">
<div class="col-md-8 mx-auto">
<component type="typeof(VaultBridgeUI)"
param-Controller="@(new NFCController()
{
NewCard = Model.NewCard,
BoltcardUrl = Model.BoltcardUrl,
PullPaymentId = Model.PullPaymentId
})"
render-mode="Server"/>
</div>
</div>
<div id="body" class="my-4"> <div id="body" class="my-4">
<form id="broadcastForm" method="post" style="display:none;"> <form id="broadcastForm" method="post" style="display:none;">
<input type="hidden" asp-for="WebsocketPath" />
<input type="hidden" asp-for="ReturnUrl" />
</form> </form>
<div id="vaultPlaceholder"></div>
<button id="vault-retry" class="btn btn-primary" style="display:none;" type="button" text-translate="true">Retry</button>
<button id="vault-confirm" class="btn btn-primary" style="display:none;"></button>
</div> </div>
<partial name="VaultElements" />
@section PageFootContent @section PageFootContent
{ {
<script src="~/js/vaultbridge.js" type="text/javascript" defer="defer" asp-append-version="true"></script> <script src="~/js/vaultbridge.js" asp-append-version="true"></script>
<script src="~/js/vaultbridge.ui.js" type="text/javascript" defer="defer" asp-append-version="true"></script>
<script> <script>
function delay(ms) { vault.done = function ()
return new Promise(resolve => setTimeout(resolve, ms)); {
}
async function askSign() {
var websocketPath = $("#WebsocketPath").val();
var loc = window.location, ws_uri;
if (loc.protocol === "https:") {
ws_uri = "wss:";
} else {
ws_uri = "ws:";
}
ws_uri += "//" + loc.host;
ws_uri += websocketPath;
var html = $("#VaultConnection").html();
$("#vaultPlaceholder").html(html);
var vaultUI = new vaultui.VaultBridgeUI(ws_uri);
var command = @Safe.Json(Model.Command);
while (!await vaultUI.sendBackendCommand(command)) {
await vaultUI.waitRetryPushed();
}
await delay(2000);
$("#broadcastForm").submit(); $("#broadcastForm").submit();
} }
document.addEventListener("DOMContentLoaded", function () {
askSign();
});
</script> </script>
} }

View File

@@ -15,41 +15,6 @@
<p class="lead text-secondary mt-3" text-translate="true">Please check that your wallet is generating the same addresses as below.</p> <p class="lead text-secondary mt-3" text-translate="true">Please check that your wallet is generating the same addresses as below.</p>
</header> </header>
@if (!ViewContext.ModelState.IsValid)
{
<div asp-validation-summary="All"></div>
}
@if (Model.Method is WalletSetupMethod.Hardware)
{
<partial name="LocalhostBrowserSupport" />
}
<template id="modal-template">
<div class="modal-dialog" role="document">
<form class="modal-content" method="post" enctype="multipart/form-data">
<div class="modal-header">
<h5 class="modal-title" id="addressVerification" text-translate="true">Address verification</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="@StringLocalizer["Close"]">
<vc:icon symbol="close" />
</button>
</div>
<div class="modal-body">
<p>
<span text-translate="true">Confirm that you see the following address on the device:</span>
<code id="displayed-address"></code>
</p>
<div id="vault-status"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" text-translate="true">Close</button>
<button id="vault-confirm" class="btn btn-primary" style="display:none;"></button>
</div>
</form>
</div>
</template>
<div id="btcpayservervault" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="btcpayservervault" aria-hidden="true"></div>
<form method="post" asp-controller="UIStores" asp-action="UpdateWallet" asp-route-storeId="@Model.StoreId" asp-route-cryptoCode="@Model.CryptoCode"> <form method="post" asp-controller="UIStores" asp-action="UpdateWallet" asp-route-storeId="@Model.StoreId" asp-route-cryptoCode="@Model.CryptoCode">
<input asp-for="Config" type="hidden"/> <input asp-for="Config" type="hidden"/>
<input asp-for="Confirmation" type="hidden"/> <input asp-for="Confirmation" type="hidden"/>
@@ -63,10 +28,6 @@
<tr> <tr>
<th text-translate="true">Key path</th> <th text-translate="true">Key path</th>
<th text-translate="true">Address</th> <th text-translate="true">Address</th>
@if (Model.Source == "Vault")
{
<th></th>
}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -75,13 +36,6 @@
<tr> <tr>
<td>@sample.KeyPath</td> <td>@sample.KeyPath</td>
<td><code>@sample.Address</code></td> <td><code>@sample.Address</code></td>
@if (Model.Source == "Vault")
{
<td class="text-end">
@* Using single quotes for the data attributes on purpose *@
<a href="#" data-address='@Safe.Json(sample.Address)' data-rooted-key-path='@Safe.Json(sample.RootedKeyPath.ToString())' text-translate="true">Show on device</a>
</td>
}
</tr> </tr>
} }
</tbody> </tbody>
@@ -93,54 +47,4 @@
</div> </div>
</form> </form>
@section PageFootContent {
<partial name="_ValidationScriptsPartial" />
<partial name="VaultElements" />
<script src="~/js/vaultbridge.js" defer asp-append-version="true"></script>
<script src="~/js/vaultbridge.ui.js" defer asp-append-version="true"></script>
<script>
window.addEventListener("load", async () => {
const wsPath = "@Url.Action("VaultBridgeConnection", "UIVault", new {cryptoCode = Model.CryptoCode})";
const wsProto = location.protocol.replace(/^http/, "ws");
const statusHTML = document.getElementById("VaultConnection").innerHTML;
const modalHTML = document.getElementById("modal-template").innerHTML;
const $modal = document.getElementById("btcpayservervault");
document.querySelectorAll("[data-address]").forEach(link => {
link.addEventListener("click", async event => {
event.preventDefault();
const $link = event.currentTarget;
const address = JSON.parse($link.dataset.address);
const rootedKeyPath = JSON.parse($link.dataset.rootedKeyPath);
$modal.innerHTML = modalHTML;
const $address = document.getElementById("displayed-address");
const $status = document.getElementById("vault-status");
$status.innerHTML = statusHTML;
$address.innerText = address;
const vaultUI = new vaultui.VaultBridgeUI(`${wsProto}//${location.host}${wsPath}`);
const $$modal = $($modal)
$$modal.modal();
$$modal.on('hidden.bs.modal', () => {
vaultUI.closeBridge();
});
$$modal.modal("show");
while (!await vaultUI.askForDevice()) {}
await vaultUI.askForDisplayAddress(rootedKeyPath);
$$modal.modal("hide");
});
});
});
</script>
}

View File

@@ -1,7 +1,14 @@
@using BTCPayServer.Abstractions.TagHelpers
@using BTCPayServer.Blazor
@using BTCPayServer.Blazor.VaultBridge
@using BTCPayServer.TagHelpers
@using Microsoft.AspNetCore.Mvc.TagHelpers
@model WalletSetupViewModel @model WalletSetupViewModel
@{ @{
Layout = "_LayoutWalletSetup"; Layout = "_LayoutWalletSetup";
ViewData.SetActivePage(StoreNavPages.OnchainSettings, StringLocalizer["Connect your hardware wallet"], $"{Context.GetStoreData().Id}-{Model.CryptoCode}"); ViewData.SetActivePage(StoreNavPages.OnchainSettings, StringLocalizer["Connect your hardware wallet"], $"{Context.GetStoreData().Id}-{Model.CryptoCode}");
this.ViewData.SetBlazorAllowed(true);
} }
@section Navbar { @section Navbar {
@@ -15,97 +22,30 @@
<p class="lead text-secondary mt-3" text-translate="true">In order to securely connect to your hardware wallet you must first download, install, and run the BTCPay Server Vault.</p> <p class="lead text-secondary mt-3" text-translate="true">In order to securely connect to your hardware wallet you must first download, install, and run the BTCPay Server Vault.</p>
</header> </header>
@if (!ViewContext.ModelState.IsValid)
{
<div asp-validation-summary="All"></div>
}
<partial name="LocalhostBrowserSupport" />
<div class="row mt-5 mb-4"> <div class="row mt-5 mb-4">
<div class="col-md-8 mx-auto"> <div class="col-md-8 mx-auto">
<div id="vault-status" class="mb-4"></div> <component type="typeof(VaultBridgeUI)"
<div id="vault-xpub" class="mt-4" style="display:none;"> param-Controller=@(new GetXPubController()
<div class="form-group"> {
<label for="addressType" class="form-label" text-translate="true">Address type</label> CryptoCode = Context.GetRouteValue("cryptoCode")?.ToString(),
<select id="addressType" name="addressType" class="form-select w-auto"> })
<option value="segwit" text-translate="true">Segwit (Recommended, cheapest fee)</option> render-mode="Server"/>
<option value="segwitWrapped" text-translate="true">Segwit wrapped (Compatible with old wallets)</option>
<option value="legacy" text-translate="true">Legacy (Not recommended)</option>
@if (ViewData["CanUseTaproot"] is true)
{
<option value="taproot" text-translate="true">Taproot (ONLY FOR DEVELOPMENT)</option>
}
</select>
</div>
<div class="form-group">
<label for="accountNumber" class="form-label" text-translate="true">Account</label>
<input id="accountNumber" class="form-control" name="accountNumber" type="number" value="0" min="0" step="1" style="max-width:12ch;" />
</div>
</div>
<div>
<button type="submit" id="vault-confirm" class="btn btn-primary" style="display:none;"></button>
<button type="button" id="vault-retry" class="btn btn-secondary" style="display:none;" text-translate="true">Retry</button>
</div>
</div> </div>
</div> </div>
<form method="post" id="walletInfo" style="display:none;"> <form id="walletInfo" method="post" asp-controller="UIStores" asp-action="UpdateWallet" asp-route-storeId="@Model.StoreId" asp-route-cryptoCode="@Model.CryptoCode">
<input asp-for="Config" type="hidden" /> <input asp-for="Config" type="hidden" />
<input asp-for="CryptoCode" type="hidden" /> <input asp-for="Confirmation" type="hidden" value="true" />
<input asp-for="AccountKey" type="hidden" />
<input asp-for="Source" type="hidden" value="Vault"/>
<input asp-for="DerivationSchemeFormat" type="hidden" value="BTCPay" />
<div class="row mb-5">
<div class="col-md-8 mx-auto">
<h4 class="mb-3" text-translate="true">Public Key Information</h4>
<div class="form-group">
<label asp-for="DerivationScheme" class="form-label"></label>
<textarea asp-for="DerivationScheme" class="form-control store-derivation-scheme font-monospace py-2" rows="3" readonly></textarea>
</div>
<div class="form-group">
<label asp-for="RootFingerprint" class="form-label"></label>
<input asp-for="RootFingerprint" class="form-control" readonly />
</div>
<div class="form-group">
<label asp-for="KeyPath" class="form-label"></label>
<input asp-for="KeyPath" class="form-control" readonly />
</div>
<button name="command" type="submit" class="btn btn-primary" value="save" id="Continue" text-translate="true">Continue</button>
</div>
</div>
</form> </form>
@section PageFootContent { @section PageFootContent {
<partial name="_ValidationScriptsPartial" /> <script src="~/js/vaultbridge.js" asp-append-version="true"></script>
<partial name="VaultElements" />
<script src="~/js/vaultbridge.js" defer asp-append-version="true"></script>
<script src="~/js/vaultbridge.ui.js" defer asp-append-version="true"></script>
<script> <script>
window.addEventListener("load", async () => { vault.setXPub = function (xpub)
const wsPath = "@Url.Action("VaultBridgeConnection", "UIVault", new {cryptoCode = Model.CryptoCode})"; {
const wsProto = location.protocol.replace(/^http/, "ws"); $("#Config").val(xpub.config);
const vaultUI = new vaultui.VaultBridgeUI(`${wsProto}//${location.host}${wsPath}`); $("#walletInfo").submit();
}
document.getElementById("vault-status").innerHTML = document.getElementById("VaultConnection").innerHTML;
window.addEventListener("beforeunload", () => {
vaultUI.closeBridge();
});
while (!await vaultUI.askForDevice() || !await vaultUI.askForXPubs()) {};
const { xpub: { strategy, fingerprint, accountKey, keyPath } } = vaultUI;
document.getElementById("DerivationScheme").value = strategy;
document.getElementById("RootFingerprint").value = fingerprint;
document.getElementById("AccountKey").value = accountKey;
document.getElementById("KeyPath").value = keyPath;
document.getElementById("walletInfo").style = null;
});
</script> </script>
} }

View File

@@ -23,7 +23,6 @@
<form method="post" class="my-5"> <form method="post" class="my-5">
<input asp-for="Config" type="hidden" /> <input asp-for="Config" type="hidden" />
<input asp-for="CryptoCode" type="hidden" /> <input asp-for="CryptoCode" type="hidden" />
<input asp-for="DerivationSchemeFormat" type="hidden" />
<input asp-for="AccountKey" type="hidden" /> <input asp-for="AccountKey" type="hidden" />
<div class="form-group"> <div class="form-group">

View File

@@ -29,7 +29,7 @@
{ {
<div class="mt-5"> <div class="mt-5">
<div class="list-group"> <div class="list-group">
<a asp-controller="UIStores" asp-action="ImportWallet" asp-route-storeId="@Model.StoreId" asp-route-cryptoCode="@Model.CryptoCode" asp-route-method="hardware" id="ImportHardwareLink" class="list-group-item list-group-item-action only-for-js"> <a asp-controller="UIStores" asp-action="ImportWallet" asp-route-storeId="@Model.StoreId" asp-route-cryptoCode="@Model.CryptoCode" asp-route-method="Hardware" id="ImportHardwareLink" class="list-group-item list-group-item-action only-for-js">
<div class="image"> <div class="image">
<vc:icon symbol="wallet-hardware"/> <vc:icon symbol="wallet-hardware"/>
</div> </div>

View File

@@ -112,14 +112,14 @@
</div> </div>
<span asp-validation-for="IsMultiSigOnServer" class="text-danger"></span> <span asp-validation-for="IsMultiSigOnServer" class="text-danger"></span>
</div> </div>
<div class="form-group my-4">
<div class="d-flex align-items-center">
<input asp-for="DefaultIncludeNonWitnessUtxo" type="checkbox" class="btcpay-toggle me-3"/>
<label asp-for="DefaultIncludeNonWitnessUtxo" class="form-check-label"></label>
</div>
<span asp-validation-for="DefaultIncludeNonWitnessUtxo" class="text-danger"></span>
</div>
} }
<div class="form-group my-4">
<div class="d-flex align-items-center">
<input asp-for="DefaultIncludeNonWitnessUtxo" type="checkbox" class="btcpay-toggle me-3"/>
<label asp-for="DefaultIncludeNonWitnessUtxo" class="form-check-label"></label>
</div>
<span asp-validation-for="DefaultIncludeNonWitnessUtxo" class="text-danger"></span>
</div>
<div class="form-group"> <div class="form-group">
<label asp-for="Label" class="form-label"></label> <label asp-for="Label" class="form-label"></label>
<input asp-for="Label" class="form-control" style="max-width:24em;" /> <input asp-for="Label" class="form-control" style="max-width:24em;" />

View File

@@ -21,3 +21,8 @@
@RenderBody() @RenderBody()
<vc:ui-extension-point location="onchain-wallet-setup-post-body" model="@Model"/> <vc:ui-extension-point location="onchain-wallet-setup-post-body" model="@Model"/>
@if (User.Identity.IsAuthenticated && ViewData.IsBlazorAllowed())
{
<script src="~/_framework/blazor.server.js" autostart="false" asp-append-version="true"></script>
}

View File

@@ -1,10 +1,14 @@
@using BTCPayServer.Controllers @using BTCPayServer.Abstractions.TagHelpers
@using BTCPayServer.Blazor.VaultBridge
@using BTCPayServer.TagHelpers
@using Microsoft.AspNetCore.Mvc.TagHelpers
@model WalletSendVaultModel @model WalletSendVaultModel
@{ @{
var walletId = Context.GetRouteValue("walletId").ToString(); var walletId = Context.GetRouteValue("walletId")?.ToString() ?? "";
Model.ReturnUrl ??= Url.WalletTransactions(walletId); Model.ReturnUrl ??= Url.WalletTransactions(walletId);
Layout = "_LayoutWizard"; Layout = "_LayoutWizard";
ViewData.SetActivePage(WalletsNavPages.Send, StringLocalizer["Sign the transaction"], walletId); ViewData.SetActivePage(WalletsNavPages.Send, StringLocalizer["Sign the transaction"], walletId);
this.ViewData.SetBlazorAllowed(true);
} }
@section Navbar { @section Navbar {
@@ -16,52 +20,37 @@
<p class="lead text-secondary mt-3" text-translate="true">Using BTCPay Server Vault</p> <p class="lead text-secondary mt-3" text-translate="true">Using BTCPay Server Vault</p>
</header> </header>
<partial name="LocalhostBrowserSupport" /> <div class="row mt-5 mb-4">
<div class="col-md-8 mx-auto">
<component type="typeof(VaultBridgeUI)"
param-Controller="@(new SignHWIController()
{
StoreId = WalletId.Parse(walletId).StoreId,
CryptoCode = WalletId.Parse(walletId).CryptoCode,
PSBT = Model.SigningContext.PSBT
})"
render-mode="Server"/>
</div>
</div>
<div id="body" class="my-4"> <div id="body" class="my-4">
<form id="broadcastForm" asp-action="WalletSendVault" asp-route-walletId="@walletId" method="post" style="display:none;"> <form id="broadcastForm" asp-action="WalletSendVault" asp-route-walletId="@walletId" method="post" style="display:none;">
<input type="hidden" id="WalletId" asp-for="WalletId" /> <input type="hidden" id="WalletId" asp-for="WalletId" />
<input type="hidden" asp-for="WebsocketPath" />
<input type="hidden" asp-for="ReturnUrl" /> <input type="hidden" asp-for="ReturnUrl" />
<input type="hidden" asp-for="BackUrl" /> <input type="hidden" asp-for="BackUrl" />
<partial name="SigningContext" for="SigningContext" /> <partial name="SigningContext" for="SigningContext" />
</form> </form>
<div id="vaultPlaceholder"></div>
<button id="vault-retry" class="btn btn-primary" style="display:none;" type="button" text-translate="true">Retry</button>
<button id="vault-confirm" class="btn btn-primary" style="display:none;"></button>
</div> </div>
<partial name="VaultElements" />
@section PageFootContent @section PageFootContent
{ {
<script src="~/js/vaultbridge.js" type="text/javascript" defer="defer" asp-append-version="true"></script> <script src="~/js/vaultbridge.js" asp-append-version="true"></script>
<script src="~/js/vaultbridge.ui.js" type="text/javascript" defer="defer" asp-append-version="true"></script>
<script> <script>
async function askSign() { vault.setSignedPSBT = function (data)
var websocketPath = $("#WebsocketPath").val(); {
var loc = window.location, ws_uri; $("#SigningContext_PSBT").val(data.psbt);
if (loc.protocol === "https:") {
ws_uri = "wss:";
} else {
ws_uri = "ws:";
}
ws_uri += "//" + loc.host;
ws_uri += websocketPath;
var html = $("#VaultConnection").html();
$("#vaultPlaceholder").html(html);
var vaultUI = new vaultui.VaultBridgeUI(ws_uri);
while (!await vaultUI.askForDevice() || !await vaultUI.askSignPSBT({
walletId: $("#WalletId").val(),
psbt: $("#SigningContext_PSBT").val()
})) {
}
$("#SigningContext_PSBT").val(vaultUI.psbt);
$("#broadcastForm").submit(); $("#broadcastForm").submit();
} }
document.addEventListener("DOMContentLoaded", function () {
askSign();
});
</script> </script>
} }

View File

@@ -1,112 +0,0 @@
using System;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using NBXplorer;
namespace BTCPayServer
{
public class WebSocketHelper
{
private readonly WebSocket _Socket;
public WebSocket Socket
{
get
{
return _Socket;
}
}
public WebSocketHelper(WebSocket socket)
{
_Socket = socket;
var buffer = new byte[ORIGINAL_BUFFER_SIZE];
_Buffer = new ArraySegment<byte>(buffer, 0, buffer.Length);
}
const int ORIGINAL_BUFFER_SIZE = 1024 * 5;
const int MAX_BUFFER_SIZE = 1024 * 1024 * 5;
readonly ArraySegment<byte> _Buffer;
readonly UTF8Encoding UTF8 = new UTF8Encoding(false, true);
public async Task<string> NextMessageAsync(CancellationToken cancellation)
{
var buffer = _Buffer;
var array = _Buffer.Array;
var originalSize = _Buffer.Array.Length;
var newSize = _Buffer.Array.Length;
while (true)
{
var message = await Socket.ReceiveAndPingAsync(buffer, cancellation);
if (message.MessageType == WebSocketMessageType.Close)
{
await CloseSocketAndThrow(WebSocketCloseStatus.NormalClosure, "Close message received from the peer", cancellation);
break;
}
if (message.MessageType != WebSocketMessageType.Text)
{
await CloseSocketAndThrow(WebSocketCloseStatus.InvalidMessageType, "Only Text is supported", cancellation);
break;
}
if (message.EndOfMessage)
{
buffer = new ArraySegment<byte>(array, 0, buffer.Offset + message.Count);
try
{
var o = UTF8.GetString(buffer.Array, 0, buffer.Count);
if (newSize != originalSize)
{
Array.Resize(ref array, originalSize);
}
return o;
}
catch (Exception ex)
{
await CloseSocketAndThrow(WebSocketCloseStatus.InvalidPayloadData, $"Invalid payload: {ex.Message}", cancellation);
}
}
else
{
if (buffer.Count - message.Count <= 0)
{
newSize *= 2;
if (newSize > MAX_BUFFER_SIZE)
await CloseSocketAndThrow(WebSocketCloseStatus.MessageTooBig, "Message is too big", cancellation);
Array.Resize(ref array, newSize);
buffer = new ArraySegment<byte>(array, buffer.Offset, newSize - buffer.Offset);
}
buffer = buffer.Slice(message.Count, buffer.Count - message.Count);
}
}
throw new InvalidOperationException("Should never happen");
}
private async Task CloseSocketAndThrow(WebSocketCloseStatus status, string description, CancellationToken cancellation)
{
var array = _Buffer.Array;
if (array.Length != ORIGINAL_BUFFER_SIZE)
Array.Resize(ref array, ORIGINAL_BUFFER_SIZE);
await Socket.CloseSocket(status, description, cancellation);
throw new WebSocketException($"The socket has been closed ({status}: {description})");
}
public async Task Send(string evt, CancellationToken cancellation = default)
{
var bytes = UTF8.GetBytes(evt);
using var cts = new CancellationTokenSource(5000);
using var cts2 = CancellationTokenSource.CreateLinkedTokenSource(cancellation);
await Socket.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, cts2.Token);
}
public async Task DisposeAsync(CancellationToken cancellation)
{
try
{
await Socket.CloseSocket(WebSocketCloseStatus.NormalClosure, "Disposing NotificationServer", cancellation);
}
catch { }
finally { try { Socket.Dispose(); } catch { } }
}
}
}

View File

@@ -1,58 +0,0 @@
function getVaultUI() {
var websocketPath = $("#WebsocketPath").text();
var loc = window.location, ws_uri;
if (loc.protocol === "https:") {
ws_uri = "wss:";
} else {
ws_uri = "ws:";
}
ws_uri += "//" + loc.host;
ws_uri += websocketPath;
return new vaultui.VaultBridgeUI(ws_uri);
}
function showModal() {
var html = $("#btcpayservervault_template").html();
$("#btcpayservervault").html(html);
html = $("#VaultConnection").html();
$("#vaultPlaceholder").html(html);
$('#btcpayservervault').modal();
}
async function showAddress(rootedKeyPath, address) {
$(".showaddress").addClass("disabled");
showModal();
$("#btcpayservervault #displayedAddress").text(address);
var vaultUI = getVaultUI();
$('#btcpayservervault').on('hidden.bs.modal', function () {
vaultUI.closeBridge();
$(".showaddress").removeClass("disabled");
});
if (await vaultUI.askForDevice())
await vaultUI.askForDisplayAddress(rootedKeyPath);
$('#btcpayservervault').modal("hide");
}
$(document).ready(function () {
function displayXPubs(xpub) {
$("#DerivationScheme").val(xpub.strategy);
$("#RootFingerprint").val(xpub.fingerprint);
$("#AccountKey").val(xpub.accountKey);
$("#Source").val("Vault");
$("#DerivationSchemeFormat").val("BTCPay");
$("#KeyPath").val(xpub.keyPath);
$(".modal").modal('hide');
$(".hw-fields").show();
}
$(".check-for-vault").on("click", async function () {
var vaultUI = getVaultUI();
showModal();
$('#btcpayservervault').on('hidden.bs.modal', function () {
vaultUI.closeBridge();
});
while (! await vaultUI.askForDevice() || ! await vaultUI.askForXPubs()) {
}
displayXPubs(vaultUI.xpub);
});
});

View File

@@ -1,118 +1,52 @@
var vault = (function () { var vault = (function () {
/** @param {WebSocket} websocket async function sendRequest(req)
*/ {
function VaultBridge(websocket) {
var self = this; try {
/** const response = await fetch(req.uri, {
* @type {WebSocket} method: 'POST',
*/ headers: {
this.socket = websocket; 'Content-Type': 'text/plain'
this.close = function () { if (websocket) websocket.close(); }; },
/** body: JSON.stringify(req.body)
* @returns {Promise} });
*/ if (!response.ok) {
this.waitBackendMessage = function () { return { httpCode: response.status };
return new Promise(function (resolve, reject) {
self.nextResolveBackendMessage = resolve;
});
};
this.socket.onmessage = function (event) {
if (typeof event.data === "string") {
if (event.data === "ping")
return;
var jsonObject = JSON.parse(event.data);
if (jsonObject.command == "sendRequest") {
var request = new XMLHttpRequest();
request.onreadystatechange = function () {
if (request.readyState == 4) {
if (request.status === 0) {
self.socket.send("{\"error\": \"Failed to connect to uri\"}");
}
else if (self.socket.readyState == 1) {
var body = null;
if (request.responseText) {
var contentType = request.getResponseHeader('Content-Type') || 'text/plain';
if (contentType === 'text/plain')
body = request.responseText;
else
body = JSON.parse(request.responseText);
}
self.socket.send(JSON.stringify(
{
httpCode: request.status,
body: body
}));
}
}
};
request.overrideMimeType("text/plain");
request.open('POST', jsonObject.uri);
jsonObject.body = jsonObject.body || {};
request.send(JSON.stringify(jsonObject.body));
}
else {
if (self.nextResolveBackendMessage)
self.nextResolveBackendMessage(jsonObject);
} }
const contentType = response.headers.get('Content-Type') || 'text/plain';
const body = contentType.includes('application/json')
? await response.json()
: await response.text();
return { httpCode: response.status, body };
} catch (e) {
return { httpCode: 0, error: e.message };
} }
}
async function askVaultPermission(url) {
url = url + "/request-permission";
var browser = "other";
if (window.safari !== undefined)
browser = "safari";
if (navigator.brave !== undefined)
browser = "brave";
try {
const response = await fetch(url, {
method: 'GET',
headers: {
'Accept': 'text/plain'
}
});
return {httpCode: response.status, browser: browser};
} catch (e) {
return {httpCode: 0, browser: browser};
}
}
return {
askVaultPermission: askVaultPermission,
sendRequest: sendRequest
}; };
} }
)();
/**
* @param {string} ws_uri
* @returns {Promise<VaultBridge>}
*/
function connectToBackendSocket(ws_uri) {
return new Promise(function (resolve, reject) {
var supportWebSocket = "WebSocket" in window && window.WebSocket.CLOSING === 2;
if (!supportWebSocket) {
reject(vault.errors.socketNotSupported);
return;
}
var socket = new WebSocket(ws_uri);
socket.onerror = function (error) {
console.warn(error);
reject(vault.errors.socketError);
};
socket.onopen = function () {
resolve(new vault.VaultBridge(socket));
};
});
}
/**
* @returns {Promise}
*/
function askVaultPermission() {
return new Promise(function (resolve, reject) {
var request = new XMLHttpRequest();
request.onreadystatechange = function () {
if (request.readyState == 4 && request.status == 200) {
resolve();
}
if (request.readyState == 4 && request.status == 0) {
reject(vault.errors.notRunning);
}
if (request.readyState == 4 && request.status == 401) {
reject(vault.errors.denied);
}
};
request.overrideMimeType("text/plain");
request.open('GET', 'http://127.0.0.1:65092/hwi-bridge/v1/request-permission');
request.send();
});
}
return {
errors: {
notRunning: "NotRunning",
denied: "Denied",
socketNotSupported: "SocketNotSupported",
socketError: "SocketError"
},
askVaultPermission: askVaultPermission,
connectToBackendSocket: connectToBackendSocket,
VaultBridge: VaultBridge
};
})();

View File

@@ -1,459 +0,0 @@
/// <reference path="vaultbridge.js" />
/// file: vaultbridge.js
var vaultui = (function () {
/**
* @param {string} type
* @param {string} txt
* @param {string} id
*/
function VaultFeedback(type, txt, id) {
var self = this;
this.type = type;
this.txt = txt;
this.id = id;
/**
* @param {string} str
* @param {string} by
*/
this.replace = function (str, by) {
return new VaultFeedback(self.type, self.txt.replace(str, by), self.id);
};
}
var VaultFeedbacks = {
vaultLoading: new VaultFeedback("?", "Checking BTCPay Server Vault is running...", "vault-loading"),
vaultDenied: new VaultFeedback("failed", "The user declined access to the vault.", "vault-denied"),
vaultGranted: new VaultFeedback("ok", "Access to vault granted by owner.", "vault-granted"),
noVault: new VaultFeedback("failed", "BTCPay Server Vault does not seem to be running, you can download it on <a target=\"_blank\" href=\"https://github.com/btcpayserver/BTCPayServer.Vault/releases/latest\">Github</a>.", "no-vault"),
noWebsockets: new VaultFeedback("failed", "Web sockets are not supported by the browser.", "no-websocket"),
errorWebsockets: new VaultFeedback("failed", "Error of the websocket while connecting to the backend.", "error-websocket"),
bridgeConnected: new VaultFeedback("ok", "BTCPayServer successfully connected to the vault.", "bridge-connected"),
vaultNeedUpdate: new VaultFeedback("failed", "Your BTCPay Server Vault version is outdated. Please <a target=\"_blank\" href=\"https://github.com/btcpayserver/BTCPayServer.Vault/releases/latest\">download</a> the latest version.", "vault-outdated"),
noDevice: new VaultFeedback("failed", "No device connected.", "no-device"),
needInitialized: new VaultFeedback("failed", "The device has not been initialized.", "need-initialized"),
fetchingDevice: new VaultFeedback("?", "Fetching device...", "fetching-device"),
deviceFound: new VaultFeedback("ok", "Device found: {{0}}", "device-selected"),
fetchingXpubs: new VaultFeedback("?", "Fetching public keys...", "fetching-xpubs"),
askXpubs: new VaultFeedback("?", "Select your address type and account", "fetching-xpubs"),
fetchedXpubs: new VaultFeedback("ok", "Public keys successfully fetched.", "xpubs-fetched"),
unexpectedError: new VaultFeedback("failed", "An unexpected error happened. ({{0}})", "unknown-error"),
invalidNetwork: new VaultFeedback("failed", "The device is targeting a different chain.", "invalid-network"),
needPin: new VaultFeedback("?", "Enter the pin.", "need-pin"),
incorrectPin: new VaultFeedback("failed", "Incorrect pin code.", "incorrect-pin"),
invalidPasswordConfirmation: new VaultFeedback("failed", "Invalid password confirmation.", "invalid-password-confirm"),
wrongWallet: new VaultFeedback("failed", "This device can't sign the transaction. (Wrong device, wrong passphrase or wrong device fingerprint in your wallet settings)", "wrong-wallet"),
wrongKeyPath: new VaultFeedback("failed", "This device can't sign the transaction. (The wallet keypath in your wallet settings seems incorrect)", "wrong-keypath"),
needPassphrase: new VaultFeedback("?", "Enter the passphrase.", "need-passphrase"),
needPassphraseOnDevice: new VaultFeedback("?", "Please, enter the passphrase on the device.", "need-passphrase-on-device"),
signingTransaction: new VaultFeedback("?", "Please review and confirm the transaction on your device...", "ask-signing"),
reviewAddress: new VaultFeedback("?", "Sending... Please review the address on your device...", "ask-signing"),
signingRejected: new VaultFeedback("failed", "The user refused to sign the transaction", "user-reject"),
alreadySignedPsbt: new VaultFeedback("failed", "This device already signed PSBT.", "already-signed-psbt"),
};
/**
* @param {string} backend_uri
*/
function VaultBridgeUI(backend_uri) {
/**
* @type {VaultBridgeUI}
*/
var self = this;
/**
* @type {string}
*/
this.backend_uri = backend_uri;
/**
* @type {vault.VaultBridge}
*/
this.bridge = null;
/**
* @type {string}
*/
this.psbt = null;
this.xpub = null;
this.retryShowing = false;
function showRetry() {
var button = $("#vault-retry");
self.retryShowing = true;
button.show();
}
this.currentFeedback = 1;
/**
* @param {VaultFeedback} feedback
*/
function show(feedback) {
const $icon = document.querySelector(`.vault-feedback.vault-feedback${self.currentFeedback} .vault-feedback-icon`);
let iconClasses = '';
if (feedback.type == "?") {
iconClasses = "icon-dots feedback-icon-loading";
}
else if (feedback.type == "ok") {
iconClasses = "icon-checkmark feedback-icon-success";
$icon.innerHTML = $icon.innerHTML.replace("#dots", "#checkmark");
}
else if (feedback.type == "failed") {
iconClasses = "icon-cross feedback-icon-failed";
$icon.innerHTML = $icon.innerHTML.replace("#dots", "#cross");
showRetry();
}
$icon.setAttribute('class', `vault-feedback-icon icon me-2 ${iconClasses}`);
const $content = document.querySelector(`.vault-feedback.vault-feedback${self.currentFeedback} .vault-feedback-content`);
$content.innerHTML = feedback.txt;
if (feedback.type === 'ok')
self.currentFeedback++;
if (feedback.type === 'failed')
self.currentFeedback = 1;
}
function showError(json) {
if (json.hasOwnProperty("error")) {
for (var key in VaultFeedbacks) {
if (VaultFeedbacks.hasOwnProperty(key) && VaultFeedbacks[key].id == json.error) {
if (VaultFeedbacks.unexpectedError === VaultFeedbacks[key]) {
show(VaultFeedbacks.unexpectedError.replace("{{0}}", json.message));
}
else {
show(VaultFeedbacks[key]);
}
if (json.hasOwnProperty("details"))
console.warn(json.details);
return;
}
}
show(VaultFeedbacks.unexpectedError.replace("{{0}}", json.message));
if (json.hasOwnProperty("details"))
console.warn(json.details);
}
}
function showMessage(message) {
let type = 'ok';
if (message.type === 'Error')
type = 'failed';
if (message.type === 'Processing')
type = '?';
show(new VaultFeedback(type, message.message, ""));
if (type.debug)
console.warn(type.debug);
}
async function needRetry(json) {
if (json.hasOwnProperty("error")) {
var handled = false;
if (json.error === "need-device") {
handled = true;
if (await self.askForDevice())
return true;
}
if (json.error === "need-pin") {
handled = true;
if (await self.askForPin())
return true;
}
if (json.error === "need-passphrase") {
handled = true;
if (await self.askForPassphrase())
return true;
}
if (json.error === "need-passphrase-on-device") {
handled = true;
show(VaultFeedbacks.needPassphraseOnDevice);
self.bridge.socket.send("ask-passphrase");
var json = await self.bridge.waitBackendMessage();
if (json.hasOwnProperty("error")) {
showError(json);
return false;
}
return true;
}
if (!handled) {
showError(json);
}
}
return false;
}
this.waitRetryPushed = function () {
var button = $("#vault-retry");
return new Promise(function (resolve) {
button.click(function () {
// Reset feedback statuses
$(".vault-feedback").each(function () {
var icon = $(this).find(".vault-feedback-icon");
icon.removeClass().addClass("vault-feedback-icon d-none");
$(this).find(".vault-feedback-content").html('');
});
button.hide();
self.retryShowing = false;
resolve(true);
});
});
};
this.ensureConnectedToBackend = async function () {
if (self.retryShowing) {
await self.waitRetryPushed();
}
if (!self.bridge || self.bridge.socket.readyState !== 1) {
$("#vault-dropdown").css("display", "none");
show(VaultFeedbacks.vaultLoading);
try {
await vault.askVaultPermission();
} catch (ex) {
if (ex == vault.errors.notRunning)
show(VaultFeedbacks.noVault);
else if (ex == vault.errors.denied)
show(VaultFeedbacks.vaultDenied);
return false;
}
show(VaultFeedbacks.vaultGranted);
try {
self.bridge = await vault.connectToBackendSocket(self.backend_uri);
show(VaultFeedbacks.bridgeConnected);
} catch (ex) {
if (ex == vault.errors.socketNotSupported)
show(VaultFeedbacks.noWebsockets);
if (ex == vault.errors.socketError)
show(VaultFeedbacks.errorWebsockets);
return false;
}
}
return true;
};
this.sendBackendCommand = async function (command) {
if (!self.bridge || self.bridge.socket.readyState !== 1) {
self.bridge = await vault.connectToBackendSocket(self.backend_uri);
}
show(VaultFeedbacks.vaultLoading);
self.bridge.socket.send(command);
while (true) {
var json = await self.bridge.waitBackendMessage();
if (json.command === 'showMessage') {
showMessage(json);
if (json.type === "Error") {
showRetry();
return false;
}
}
if (json.command == 'done') {
return true;
}
}
}
this.askForDisplayAddress = async function (rootedKeyPath) {
if (!await self.ensureConnectedToBackend())
return false;
show(VaultFeedbacks.reviewAddress);
self.bridge.socket.send("display-address");
self.bridge.socket.send(rootedKeyPath);
var json = await self.bridge.waitBackendMessage();
if (json.hasOwnProperty("error")) {
if (await needRetry(json))
return await self.askForDisplayAddress(rootedKeyPath);
return false;
}
return true;
}
this.askForDevice = async function () {
if (!await self.ensureConnectedToBackend())
return false;
show(VaultFeedbacks.fetchingDevice);
self.bridge.socket.send("ask-device");
var json = await self.bridge.waitBackendMessage();
if (json.hasOwnProperty("error")) {
showError(json);
return false;
}
show(VaultFeedbacks.deviceFound.replace("{{0}}", json.model));
return true;
};
this.askForXPubs = async function () {
if (!await self.ensureConnectedToBackend())
return false;
self.bridge.socket.send("ask-xpub");
var json = await self.bridge.waitBackendMessage();
if (json.hasOwnProperty("error")) {
if (await needRetry(json))
return await self.askForXPubs();
return false;
}
try {
var selectedXPubs = await self.getXpubSettings();
self.bridge.socket.send(JSON.stringify(selectedXPubs));
show(VaultFeedbacks.fetchingXpubs);
json = await self.bridge.waitBackendMessage();
if (json.hasOwnProperty("error")) {
if (await needRetry(json))
return await self.askForXPubs();
return false;
}
show(VaultFeedbacks.fetchedXpubs);
self.xpub = json;
return true;
} catch (err) {
showError({ error: true, message: err });
}
};
/**
* @returns {Promise<{addressType:string, accountNumber:number}>}
*/
this.getXpubSettings = function () {
show(VaultFeedbacks.askXpubs);
$("#vault-xpub").css("display", "block");
$("#vault-confirm").css("display", "block");
$("#vault-confirm").text("Confirm");
return new Promise(function (resolve, reject) {
$("#vault-confirm").click(async function (e) {
e.preventDefault();
$("#vault-xpub").css("display", "none");
$("#vault-confirm").css("display", "none");
$(this).unbind();
const addressType = $("select[name=\"addressType\"]").val();
const accountNumber = parseInt($("input[name=\"accountNumber\"]").val());
if (addressType && !isNaN(accountNumber)) {
resolve({ addressType, accountNumber });
} else {
reject("Provide an address type and account number")
}
});
});
};
/**
* @returns {Promise<string>}
*/
this.getUserEnterPin = function () {
show(VaultFeedbacks.needPin);
$("#pin-input").css("display", "block");
$("#vault-confirm").css("display", "block");
$("#vault-confirm").text("Confirm the pin code");
return new Promise(function (resolve, reject) {
var pinCode = "";
$("#vault-confirm").click(async function (e) {
e.preventDefault();
$("#pin-input").css("display", "none");
$("#vault-confirm").css("display", "none");
$(this).unbind();
$(".pin-button").unbind();
$("#pin-display-delete").unbind();
resolve(pinCode);
});
$("#pin-display-delete").click(function () {
pinCode = "";
$("#pin-display").val("");
});
$(".pin-button").click(function () {
var id = $(this).attr('id').replace("pin-", "");
pinCode = pinCode + id;
$("#pin-display").val($("#pin-display").val() + "*");
});
});
};
/**
* @returns {Promise<string>}
*/
this.getUserPassphrase = function () {
show(VaultFeedbacks.needPassphrase);
$("#passphrase-input").css("display", "block");
$("#vault-confirm").css("display", "block");
$("#vault-confirm").text("Confirm the passphrase");
return new Promise(function (resolve, reject) {
$("#vault-confirm").click(async function (e) {
e.preventDefault();
var passphrase = $("#Password").val();
if (passphrase !== $("#PasswordConfirmation").val()) {
show(VaultFeedbacks.invalidPasswordConfirmation);
return;
}
$("#passphrase-input").css("display", "none");
$("#vault-confirm").css("display", "none");
$(this).unbind();
resolve(passphrase);
});
});
};
this.askForPassphrase = async function () {
if (!await self.ensureConnectedToBackend())
return false;
var passphrase = await self.getUserPassphrase();
self.bridge.socket.send("set-passphrase");
self.bridge.socket.send(passphrase);
return true;
}
/**
* @returns {Promise}
*/
this.askForPin = async function () {
if (!await self.ensureConnectedToBackend())
return false;
self.bridge.socket.send("ask-pin");
var json = await self.bridge.waitBackendMessage();
if (json.hasOwnProperty("error")) {
if (json.error == "device-already-unlocked")
return true;
if (await needRetry(json))
return await self.askForPin();
return false;
}
var pinCode = await self.getUserEnterPin();
self.bridge.socket.send(pinCode);
var json = await self.bridge.waitBackendMessage();
if (json.hasOwnProperty("error")) {
showError(json);
return false;
}
return true;
}
/**
* @returns {Promise<Boolean>}
*/
this.askSignPSBT = async function (args) {
if (!await self.ensureConnectedToBackend())
return false;
show(VaultFeedbacks.signingTransaction);
self.bridge.socket.send("ask-sign");
var json = await self.bridge.waitBackendMessage();
if (json.hasOwnProperty("error")) {
if (await needRetry(json))
return await self.askSignPSBT(args);
return false;
}
self.bridge.socket.send(JSON.stringify(args));
json = await self.bridge.waitBackendMessage();
if (json.hasOwnProperty("error")) {
if (await needRetry(json))
return await self.askSignPSBT(args);
return false;
}
self.psbt = json.psbt;
return true;
};
this.closeBridge = function () {
if (self.bridge) {
self.bridge.close();
}
};
}
return {
VaultFeedback: VaultFeedback,
VaultBridgeUI: VaultBridgeUI
};
})();