mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-17 05:54:26 +01:00
Refactor vault (#6678)
* Use Blazor for the Vault code * Put elements in different file * Controller abstraction * Break into VaultElement
This commit is contained in:
@@ -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))));
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
64
BTCPayServer/Blazor/VaultBridge/Elements/Feedback.razor
Normal file
64
BTCPayServer/Blazor/VaultBridge/Elements/Feedback.razor
Normal 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; }
|
||||||
|
}
|
||||||
8
BTCPayServer/Blazor/VaultBridge/Elements/FeedbackType.cs
Normal file
8
BTCPayServer/Blazor/VaultBridge/Elements/FeedbackType.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
namespace BTCPayServer.Blazor.VaultBridge.Elements;
|
||||||
|
|
||||||
|
public enum FeedbackType
|
||||||
|
{
|
||||||
|
Loading,
|
||||||
|
Success,
|
||||||
|
Failed
|
||||||
|
}
|
||||||
69
BTCPayServer/Blazor/VaultBridge/Elements/Passphrase.razor
Normal file
69
BTCPayServer/Blazor/VaultBridge/Elements/Passphrase.razor
Normal 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();
|
||||||
|
}
|
||||||
104
BTCPayServer/Blazor/VaultBridge/Elements/PinInput.razor
Normal file
104
BTCPayServer/Blazor/VaultBridge/Elements/PinInput.razor
Normal 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();
|
||||||
|
}
|
||||||
18
BTCPayServer/Blazor/VaultBridge/Elements/Retry.razor
Normal file
18
BTCPayServer/Blazor/VaultBridge/Elements/Retry.razor
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
62
BTCPayServer/Blazor/VaultBridge/Elements/VerifyAddress.razor
Normal file
62
BTCPayServer/Blazor/VaultBridge/Elements/VerifyAddress.razor
Normal 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();
|
||||||
|
}
|
||||||
23
BTCPayServer/Blazor/VaultBridge/Elements/Warning.razor
Normal file
23
BTCPayServer/Blazor/VaultBridge/Elements/Warning.razor
Normal 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; }
|
||||||
|
}
|
||||||
81
BTCPayServer/Blazor/VaultBridge/Elements/XPubSelect.razor
Normal file
81
BTCPayServer/Blazor/VaultBridge/Elements/XPubSelect.razor
Normal 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();
|
||||||
|
}
|
||||||
307
BTCPayServer/Blazor/VaultBridge/HWIController.cs
Normal file
307
BTCPayServer/Blazor/VaultBridge/HWIController.cs
Normal 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()) });
|
||||||
|
}
|
||||||
|
}
|
||||||
9
BTCPayServer/Blazor/VaultBridge/IController.cs
Normal file
9
BTCPayServer/Blazor/VaultBridge/IController.cs
Normal 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);
|
||||||
|
}
|
||||||
|
|
||||||
146
BTCPayServer/Blazor/VaultBridge/NFCController.cs
Normal file
146
BTCPayServer/Blazor/VaultBridge/NFCController.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
122
BTCPayServer/Blazor/VaultBridge/VaultBridgeUI.razor
Normal file
122
BTCPayServer/Blazor/VaultBridge/VaultBridgeUI.razor
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
55
BTCPayServer/Blazor/VaultBridge/VaultController.cs
Normal file
55
BTCPayServer/Blazor/VaultBridge/VaultController.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
BTCPayServer/Blazor/VaultBridge/VaultElement.cs
Normal file
11
BTCPayServer/Blazor/VaultBridge/VaultElement.cs
Normal 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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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.": "",
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
@{
|
@{
|
||||||
Layout = "_LayoutSimple";
|
Layout = "_LayoutSimple";
|
||||||
ViewData.SetBlazorAllowed(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@section PageHeadContent {
|
@section PageHeadContent {
|
||||||
|
|||||||
@@ -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>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;" />
|
||||||
|
|||||||
@@ -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>
|
||||||
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { } }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
Reference in New Issue
Block a user