Compare commits

...

27 Commits

Author SHA1 Message Date
2b9f390c64 update translation 2019-11-21 14:17:27 +09:00
ee42d5c7b4 bump 2019-11-21 14:15:04 +09:00
f809dd51a6 Merge pull request #1152 from NicolasDorier/feature/vault
Add hardware support via BTCPayServer Vault
2019-11-21 14:13:43 +09:00
1a8d6e5c05 Implement BTCPayServer vault derivation scheme import 2019-11-21 14:06:16 +09:00
869ba745b2 Merge pull request #1175 from bolatovumar/fix-1169
Add CSS variable for preformatted text color
2019-11-21 14:03:05 +09:00
180dfb6edf Add CSS variable for preformatted text color
fix #1169
2019-11-20 11:12:31 -08:00
45b08ac8d2 Add sponsor 2019-11-19 20:37:15 +09:00
9a4b385432 Add sponsor 2019-11-19 20:33:26 +09:00
08289b89c5 Merge pull request #1176 from pavlenex/supporters-sw
Add new supporter to readme
2019-11-19 20:32:42 +09:00
a31d1d81c8 Update README.md 2019-11-19 09:20:31 +01:00
e4c7bb0378 add wallet of satoshi to readme
add wallet of Satoshi, fix Lunanode spacing
2019-11-19 09:16:04 +01:00
374aaf2e2b add walletofsatoshi svg logo 2019-11-19 09:12:38 +01:00
52fd686993 Merge pull request #1174 from pavlenex/new-supporters-lw
add new supporter to readme
2019-11-19 00:31:06 +09:00
03c36ef0d2 add lunanode to readme 2019-11-18 15:55:56 +01:00
71a6ffac2e Adjust status message for WalletTransactions 2019-11-18 21:47:09 +09:00
92777ba181 Make sure SSH does not throw in separate thread 2019-11-18 17:15:40 +09:00
81843fb609 Do not delete the password from the seed 2019-11-18 17:00:54 +09:00
3af3ffd038 Merge pull request #1167 from dennisreimann/ui-improvements
UI: Payment request improvement
2019-11-18 14:23:51 +09:00
2ce5cd0b6f Merge pull request #1171 from bitcoinbrisbane/master
Add office 365 to the quick fill settings
2019-11-18 14:23:12 +09:00
d791fd59e9 Merge branch 'master' of github.com:bitcoinbrisbane/btcpayserver 2019-11-18 08:12:30 +10:00
6064e3ce55 Add office365 quick settings 2019-11-18 08:12:06 +10:00
0fcfe0e977 Can prune wallet transaction history 2019-11-17 17:13:09 +09:00
997df5c64d Remove build warnings 2019-11-17 16:50:28 +09:00
27af96662f Fix bug for Network not having a NBitcoin Network 2019-11-17 13:04:42 +09:00
0be6f3ca70 UI: Remove superfluous spaces when description is empty 2019-11-16 23:28:53 +01:00
7af80611b6 UI: Better payment request amounts display
Uses a table instead of list group items, so that the columns properly align (rows use the same grid). Also aligns the values on the right.
2019-11-16 22:59:51 +01:00
929b5c7951 Add display attributes. Fix #98 2019-08-24 15:39:43 +10:00
37 changed files with 1326 additions and 123 deletions

View File

@ -126,7 +126,7 @@ namespace BTCPayServer
public virtual T ToObject<T>(string json)
{
return JsonConvert.DeserializeObject<T>(json);
return NBitcoin.JsonConverters.Serializer.ToObject<T>(json, null);
}
public virtual string ToString<T>(T obj)

View File

@ -2,7 +2,7 @@
<Import Project="../Build/Version.csproj" Condition="Exists('../Build/Version.csproj')" />
<Import Project="../Build/Common.csproj" />
<ItemGroup Condition="'$(TargetFramework)' == 'netcoreapp2.1'">
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.9" />
<PackageReference Include="Microsoft.AspNetCore.App" AllowExplicitVersion="true" Version="2.1.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="2.1.2" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="2.1.2" />

View File

@ -27,12 +27,13 @@
<EmbeddedResource Include="Currencies.txt" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="BTCPayServer.Hwi" Version="1.1.2" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.1.5" />
<PackageReference Include="BuildBundlerMinifier" Version="3.1.430" />
<PackageReference Include="BundlerMinifier.Core" Version="3.1.430" />
<PackageReference Include="BundlerMinifier.TagHelpers" Version="3.1.430" />
<PackageReference Include="HtmlSanitizer" Version="4.0.217" />
<PackageReference Include="LedgerWallet" Version="2.0.0.3" />
<PackageReference Include="LedgerWallet" Version="2.0.0.5" />
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />
<PackageReference Include="Microsoft.NetCore.Analyzers" Version="2.6.2">
<PrivateAssets>all</PrivateAssets>
@ -56,7 +57,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.9" Condition="'$(TargetFramework)' == 'netcoreapp2.1'" />
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.9" AllowExplicitVersion="true" Condition="'$(TargetFramework)' == 'netcoreapp2.1'" />
<PackageReference Include="TwentyTwenty.Storage" Version="2.11.2" />
<PackageReference Include="TwentyTwenty.Storage.Amazon" Version="2.11.2" />
<PackageReference Include="TwentyTwenty.Storage.Azure" Version="2.11.2" />
@ -201,6 +202,9 @@
<Content Update="Views\Wallets\WalletRescan.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Wallets\WalletSendVault.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Wallets\WalletSendLedger.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>

View File

@ -18,6 +18,7 @@ using Microsoft.AspNetCore.Mvc;
using NBitcoin;
using NBXplorer.DerivationStrategy;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Controllers
{
@ -119,8 +120,8 @@ namespace BTCPayServer.Controllers
return new EmptyResult();
}
private void SetExistingValues(StoreData store, DerivationSchemeViewModel vm)
{
var derivation = GetExistingDerivationStrategy(vm.CryptoCode, store);
@ -140,8 +141,6 @@ namespace BTCPayServer.Controllers
.FirstOrDefault(d => d.PaymentId == id);
return existing;
}
[HttpPost]
[Route("{storeId}/derivations/{cryptoCode}")]
@ -162,7 +161,7 @@ namespace BTCPayServer.Controllers
vm.Network = network;
vm.RootKeyPath = network.GetRootKeyPath();
DerivationSchemeSettings strategy = null;
var wallet = _WalletProvider.GetWallet(network);
if (wallet == null)
{
@ -246,7 +245,7 @@ namespace BTCPayServer.Controllers
var willBeExcluded = !vm.Enabled;
var showAddress = // Show addresses if:
// - If the user is testing the hint address in confirmation screen
// - If the user is testing the hint address in confirmation screen
(vm.Confirmation && !string.IsNullOrWhiteSpace(vm.HintAddress)) ||
// - The user is clicking on continue after changing the config
(!vm.Confirmation && oldConfig != vm.Config) ||
@ -280,7 +279,7 @@ namespace BTCPayServer.Controllers
{
TempData[WellKnownTempData.SuccessMessage] = $"Derivation settings for {network.CryptoCode} has been modified.";
}
return RedirectToAction(nameof(UpdateStore), new {storeId = storeId});
return RedirectToAction(nameof(UpdateStore), new { storeId = storeId });
}
else if (!string.IsNullOrEmpty(vm.HintAddress))
{

View File

@ -0,0 +1,261 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.ModelBinders;
using BTCPayServer.Models;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Security;
using BTCPayServer.Services;
using LedgerWallet;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using NBitcoin;
using NBXplorer.DerivationStrategy;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Controllers
{
[Route("vault")]
public class VaultController : Controller
{
private readonly IAuthorizationService _authorizationService;
public VaultController(BTCPayNetworkProvider networks, IAuthorizationService authorizationService)
{
Networks = networks;
_authorizationService = authorizationService;
}
public BTCPayNetworkProvider Networks { get; }
[HttpGet]
[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;
using (var cts = new CancellationTokenSource(TimeSpan.FromMinutes(10)))
{
var cancellationToken = cts.Token;
var network = Networks.GetNetwork<BTCPayNetwork>(cryptoCode);
if (network == null)
return NotFound();
var websocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
var hwi = new Hwi.HwiClient(network.NBitcoinNetwork)
{
Transport = new HwiWebSocketTransport(websocket)
};
Hwi.HwiDeviceClient device = null;
HDFingerprint? fingerprint = null;
var websocketHelper = new WebSocketHelper(websocket);
JObject o = null;
try
{
while (true)
{
var command = await websocketHelper.NextMessageAsync(cancellationToken);
switch (command)
{
case "ask-sign":
if (device == null)
{
await websocketHelper.Send("{ \"error\": \"need-device\"}", cancellationToken);
continue;
}
if (walletId == null)
{
await websocketHelper.Send("{ \"error\": \"invalid-walletId\"}", cancellationToken);
continue;
}
if (fingerprint is null)
{
try
{
fingerprint = (await device.GetXPubAsync(new KeyPath("44'"), cancellationToken)).ExtPubKey.ParentFingerprint;
}
catch (Hwi.HwiException ex) when (ex.ErrorCode == Hwi.HwiErrorCode.DeviceNotReady)
{
await websocketHelper.Send("{ \"error\": \"need-pin\"}", cancellationToken);
continue;
}
}
await websocketHelper.Send("{ \"info\": \"ready\"}", cancellationToken);
o = JObject.Parse(await websocketHelper.NextMessageAsync(cancellationToken));
var authorization = await _authorizationService.AuthorizeAsync(User, Policies.CanModifyStoreSettings.Key);
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);
var signing = derivationSettings.GetSigningAccountKeySettings();
if (signing.GetRootedKeyPath()?.MasterFingerprint != fingerprint)
{
await websocketHelper.Send("{ \"error\": \"wrong-wallet\"}", cancellationToken);
continue;
}
try
{
psbt = await device.SignPSBTAsync(psbt, cancellationToken);
}
catch (Hwi.HwiException ex) when (ex.ErrorCode == Hwi.HwiErrorCode.DeviceNotReady)
{
await websocketHelper.Send("{ \"error\": \"need-pin\"}", cancellationToken);
continue;
}
catch (Hwi.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 "ask-pin":
if (device == null)
{
await websocketHelper.Send("{ \"error\": \"need-device\"}", cancellationToken);
continue;
}
await device.PromptPinAsync(cancellationToken);
await websocketHelper.Send("{ \"info\": \"prompted, please input the pin\"}", cancellationToken);
o = JObject.Parse(await websocketHelper.NextMessageAsync(cancellationToken));
var pin = (int)o["pinCode"].Value<long>();
var passphrase = o["passphrase"].Value<string>();
device.Password = passphrase;
if (await device.SendPinAsync(pin, cancellationToken))
{
await websocketHelper.Send("{ \"info\": \"the pin is correct\"}", cancellationToken);
}
else
{
await websocketHelper.Send("{ \"error\": \"incorrect-pin\"}", cancellationToken);
continue;
}
break;
case "ask-xpubs":
if (device == null)
{
await websocketHelper.Send("{ \"error\": \"need-device\"}", cancellationToken);
continue;
}
JObject result = new JObject();
var factory = network.NBXplorerNetwork.DerivationStrategyFactory;
var keyPath = new KeyPath("84'").Derive(network.CoinType).Derive(0, true);
BitcoinExtPubKey xpub = null;
try
{
xpub = await device.GetXPubAsync(keyPath);
}
catch (Hwi.HwiException ex) when (ex.ErrorCode == Hwi.HwiErrorCode.DeviceNotReady)
{
await websocketHelper.Send("{ \"error\": \"need-pin\"}", cancellationToken);
continue;
}
if (fingerprint is null)
{
fingerprint = (await device.GetXPubAsync(new KeyPath("44'"), cancellationToken)).ExtPubKey.ParentFingerprint;
}
result["fingerprint"] = fingerprint.Value.ToString();
var strategy = factory.CreateDirectDerivationStrategy(xpub, new DerivationStrategyOptions()
{
ScriptPubKeyType = ScriptPubKeyType.Segwit
});
AddDerivationSchemeToJson("segwit", result, keyPath, xpub, strategy);
keyPath = new KeyPath("49'").Derive(network.CoinType).Derive(0, true);
xpub = await device.GetXPubAsync(keyPath);
strategy = factory.CreateDirectDerivationStrategy(xpub, new DerivationStrategyOptions()
{
ScriptPubKeyType = ScriptPubKeyType.SegwitP2SH
});
AddDerivationSchemeToJson("segwitWrapped", result, keyPath, xpub, strategy);
keyPath = new KeyPath("44'").Derive(network.CoinType).Derive(0, true);
xpub = await device.GetXPubAsync(keyPath);
strategy = factory.CreateDirectDerivationStrategy(xpub, new DerivationStrategyOptions()
{
ScriptPubKeyType = ScriptPubKeyType.Legacy
});
AddDerivationSchemeToJson("legacy", result, keyPath, xpub, strategy);
await websocketHelper.Send(result.ToString(), cancellationToken);
break;
case "ask-device":
var devices = (await hwi.EnumerateDevicesAsync(cancellationToken)).ToList();
device = devices.FirstOrDefault();
if (device == null)
{
await websocketHelper.Send("{ \"error\": \"no-device\"}", cancellationToken);
continue;
}
fingerprint = device.Fingerprint;
JObject json = new JObject();
json.Add("model", device.Model.ToString());
json.Add("fingerprint", device.Fingerprint?.ToString());
await websocketHelper.Send(json.ToString(), cancellationToken);
break;
}
}
}
catch (Exception ex)
{
JObject obj = new JObject();
obj.Add("error", "unknown-error");
obj.Add("details", ex.ToString());
try
{
await websocketHelper.Send(obj.ToString(), cancellationToken);
}
catch { }
}
finally
{
await websocketHelper.DisposeAsync(cancellationToken);
}
}
return new EmptyResult();
}
public StoreData CurrentStore
{
get
{
return HttpContext.GetStoreData();
}
}
private DerivationSchemeSettings GetDerivationSchemeSettings(WalletId walletId)
{
var paymentMethod = CurrentStore
.GetSupportedPaymentMethods(Networks)
.OfType<DerivationSchemeSettings>()
.FirstOrDefault(p => p.PaymentId.PaymentType == Payments.PaymentTypes.BTCLike && p.PaymentId.CryptoCode == walletId.CryptoCode);
return paymentMethod;
}
private void AddDerivationSchemeToJson(string propertyName, JObject result, KeyPath keyPath, BitcoinExtPubKey xpub, DerivationStrategyBase strategy)
{
result.Add(new JProperty(propertyName, new JObject()
{
new JProperty("strategy", strategy.ToString()),
new JProperty("accountKey", xpub.ToString()),
new JProperty("keyPath", keyPath.ToString()),
}));
}
}
}

View File

@ -60,7 +60,7 @@ namespace BTCPayServer.Controllers
vm.Decoded = psbt.ToString();
vm.PSBT = psbt.ToBase64();
}
return View(vm ?? new WalletPSBTViewModel());
return View(vm ?? new WalletPSBTViewModel() { CryptoCode = walletId.CryptoCode });
}
[HttpPost]
[Route("{walletId}/psbt")]
@ -88,6 +88,8 @@ namespace BTCPayServer.Controllers
vm.PSBT = psbt.ToBase64();
vm.FileName = vm.UploadedPSBTFile?.FileName;
return View(vm);
case "vault":
return ViewVault(walletId, psbt);
case "ledger":
return ViewWalletSendLedger(psbt);
case "update":
@ -156,7 +158,8 @@ namespace BTCPayServer.Controllers
private async Task FetchTransactionDetails(DerivationSchemeSettings derivationSchemeSettings, WalletPSBTReadyViewModel vm, BTCPayNetwork network)
{
var psbtObject = PSBT.Parse(vm.PSBT, network.NBitcoinNetwork);
psbtObject = await UpdatePSBT(derivationSchemeSettings, psbtObject, network) ?? psbtObject;
if (!psbtObject.IsAllFinalized())
psbtObject = await UpdatePSBT(derivationSchemeSettings, psbtObject, network) ?? psbtObject;
IHDKey signingKey = null;
RootedKeyPath signingKeyPath = null;
try

View File

@ -449,6 +449,8 @@ namespace BTCPayServer.Controllers
switch (command)
{
case "vault":
return ViewVault(walletId, psbt.PSBT);
case "ledger":
return ViewWalletSendLedger(psbt.PSBT, psbt.ChangeAddress);
case "seed":
@ -463,6 +465,16 @@ namespace BTCPayServer.Controllers
}
private IActionResult ViewVault(WalletId walletId, PSBT psbt)
{
return View("WalletSendVault", new WalletSendVaultModel()
{
WalletId = walletId.ToString(),
PSBT = psbt.ToBase64(),
WebsocketPath = this.Url.Action(nameof(VaultController.VaultBridgeConnection), "Vault", new { walletId = walletId.ToString() })
});
}
private IActionResult RedirectToWalletPSBT(WalletId walletId, PSBT psbt, string fileName = null)
{
var vm = new PostRedirectViewModel()
@ -873,27 +885,48 @@ namespace BTCPayServer.Controllers
[HttpPost]
public async Task<IActionResult> WalletSettings(
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, WalletSettingsViewModel vm)
WalletId walletId, WalletSettingsViewModel vm, string command = "save", CancellationToken cancellationToken = default)
{
if (!ModelState.IsValid)
return View(vm);
var derivationScheme = GetDerivationSchemeSettings(walletId);
if (derivationScheme == null)
return NotFound();
derivationScheme.Label = vm.Label;
derivationScheme.SigningKey = string.IsNullOrEmpty(vm.SelectedSigningKey) ? null : new BitcoinExtPubKey(vm.SelectedSigningKey, derivationScheme.Network.NBitcoinNetwork);
for (int i = 0; i < derivationScheme.AccountKeySettings.Length; i++)
if (command == "save")
{
derivationScheme.AccountKeySettings[i].AccountKeyPath = string.IsNullOrWhiteSpace(vm.AccountKeys[i].AccountKeyPath) ? null
: new KeyPath(vm.AccountKeys[i].AccountKeyPath);
derivationScheme.AccountKeySettings[i].RootFingerprint = string.IsNullOrWhiteSpace(vm.AccountKeys[i].MasterFingerprint) ? (HDFingerprint?)null
: new HDFingerprint(Encoders.Hex.DecodeData(vm.AccountKeys[i].MasterFingerprint));
derivationScheme.Label = vm.Label;
derivationScheme.SigningKey = string.IsNullOrEmpty(vm.SelectedSigningKey) ? null : new BitcoinExtPubKey(vm.SelectedSigningKey, derivationScheme.Network.NBitcoinNetwork);
for (int i = 0; i < derivationScheme.AccountKeySettings.Length; i++)
{
derivationScheme.AccountKeySettings[i].AccountKeyPath = string.IsNullOrWhiteSpace(vm.AccountKeys[i].AccountKeyPath) ? null
: new KeyPath(vm.AccountKeys[i].AccountKeyPath);
derivationScheme.AccountKeySettings[i].RootFingerprint = string.IsNullOrWhiteSpace(vm.AccountKeys[i].MasterFingerprint) ? (HDFingerprint?)null
: new HDFingerprint(Encoders.Hex.DecodeData(vm.AccountKeys[i].MasterFingerprint));
}
var store = (await Repository.FindStore(walletId.StoreId, GetUserId()));
store.SetSupportedPaymentMethod(derivationScheme);
await Repository.UpdateStore(store);
TempData[WellKnownTempData.SuccessMessage] = "Wallet settings updated";
return RedirectToAction(nameof(WalletSettings));
}
else if (command == "prune")
{
var result = await ExplorerClientProvider.GetExplorerClient(walletId.CryptoCode).PruneAsync(derivationScheme.AccountDerivation, cancellationToken);
if (result.TotalPruned == 0)
{
TempData[WellKnownTempData.SuccessMessage] = $"The wallet is already pruned";
}
else
{
TempData[WellKnownTempData.SuccessMessage] = $"The wallet has been successfully pruned ({result.TotalPruned} transactions have been removed from the history)";
}
return RedirectToAction(nameof(WalletSettings));
}
else
{
return NotFound();
}
var store = (await Repository.FindStore(walletId.StoreId, GetUserId()));
store.SetSupportedPaymentMethod(derivationScheme);
await Repository.UpdateStore(store);
TempData[WellKnownTempData.SuccessMessage] = "Wallet settings updated";
return RedirectToAction(nameof(WalletSettings));
}
}

View File

@ -71,22 +71,26 @@ namespace BTCPayServer
var tcs = new TaskCompletionSource<SSHCommandResult>(TaskCreationOptions.RunContinuationsAsynchronously);
new Thread(() =>
{
sshCommand.BeginExecute(ar =>
try
{
try
sshCommand.BeginExecute(ar =>
{
sshCommand.EndExecute(ar);
tcs.TrySetResult(CreateSSHCommandResult(sshCommand));
}
catch (Exception ex)
{
tcs.TrySetException(ex);
}
finally
{
sshCommand.Dispose();
}
});
try
{
sshCommand.EndExecute(ar);
tcs.TrySetResult(CreateSSHCommandResult(sshCommand));
}
catch (Exception ex)
{
tcs.TrySetException(ex);
}
finally
{
sshCommand.Dispose();
}
});
}
catch(Exception ex) { tcs.TrySetException(ex); }
})
{ IsBackground = true }.Start();
return tcs.Task;

View File

@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
namespace BTCPayServer
{
public class HwiWebSocketTransport : Hwi.Transports.ITransport
{
private readonly WebSocketHelper _webSocket;
public HwiWebSocketTransport(WebSocket webSocket)
{
_webSocket = new WebSocketHelper(webSocket);
}
public async Task<string> SendCommandAsync(string[] arguments, CancellationToken cancel)
{
JObject request = new JObject();
request.Add("params", new JArray(arguments));
await _webSocket.Send(request.ToString(), cancel);
return await _webSocket.NextMessageAsync(cancel);
}
}
}

View File

@ -22,7 +22,7 @@ namespace BTCPayServer.Models.ServerViewModels
var removedDate = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ssZ", CultureInfo.InvariantCulture);
var seedFile = new LndSeedFile
{
wallet_password = "",
wallet_password = WalletPassword,
cipher_seed_mnemonic = new List<string> { $"Seed removed on {removedDate}" }
};
var json = JsonConvert.SerializeObject(seedFile);
@ -47,7 +47,13 @@ namespace BTCPayServer.Models.ServerViewModels
{
var unlockFileContents = File.ReadAllText(lndSeedFilePath);
var unlockFile = JsonConvert.DeserializeObject<LndSeedFile>(unlockFileContents);
#pragma warning disable CA1820 // Test for empty strings using string length
if (unlockFile.wallet_password == string.Empty)
#pragma warning restore CA1820 // Test for empty strings using string length
{
// Nicolas stupidinly deleted the password, so we should use the default one here...
unlockFile.wallet_password = "hellorockstar";
}
if (unlockFile.wallet_password != null)
{
return new LndSeedBackupViewModel

View File

@ -10,6 +10,7 @@ namespace BTCPayServer.Models.WalletViewModels
{
public class WalletPSBTViewModel
{
public string CryptoCode { get; set; }
public string Decoded { get; set; }
string _FileName;
public string FileName

View File

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Models.WalletViewModels
{
public class WalletSendVaultModel
{
public string WalletId { get; set; }
public string PSBT { get; set; }
public string WebsocketPath { get; set; }
}
}

View File

@ -127,6 +127,22 @@
<a href="https://acinq.co/" class="text-muted small" target="_blank">ACINQ</a>
</div>
</div>
<div class="figure ml-4">
<a href="https://lunanode.com/" target="_blank">
<img src="~/img/lunanode.svg" alt="Sponsor LunaNode" height="75" />
</a>
<div class="figure-caption text-center">
<a href="https://lunanode.com/" class="text-muted small" target="_blank">LunaNode</a>
</div>
</div>
<div class="figure ml-4">
<a href="https://walletofsatoshi.com/" target="_blank">
<img src="~/img/walletofsatoshi.svg" alt="Sponsor Wallet of Satoshi" height="75" />
</a>
<div class="figure-caption text-center">
<a href="https://walletofsatoshi.com/" class="text-muted small" target="_blank">Wallet of Satoshi</a>
</div>
</div>
</div>
<div class="col-md-5 order-md-2 order-1">
@RenderBody()

View File

@ -4,45 +4,43 @@
<div class="row w-100 p-0 m-0" style="height: 100vh">
<div class="mx-auto my-auto w-100">
<div class="card">
<h1 class="card-header">
<h1 class="card-header px-3">
@Model.Title
<span class="text-muted float-right text-center">@Model.Status</span>
</h1>
<div class="card-body px-0 pt-0">
<div class="row mb-4">
<div class="col-sm-12 col-md-12 col-lg-6 ">
<ul class="w-100 list-group list-group-flush">
<li class="list-group-item list-group-item-light">
<div class="d-flex justify-content-between">
<span class="h2 text-muted">Request amount:</span>
<span class="h2">@Model.AmountFormatted</span>
</div>
</li>
<li class="list-group-item list-group-item-light">
<div class="d-flex justify-content-between">
<span class="h2 text-muted">Paid so far:</span>
<span class="h2">@Model.AmountCollectedFormatted</span>
</div>
</li>
<li class="list-group-item list-group-item-light">
<div class="d-flex justify-content-between">
<span class="h2 text-muted">Amount due:</span>
<span class="h2">@Model.AmountDueFormatted</span>
</div>
</li>
</ul>
<div class="w-100 p-2">@Safe.Raw(Model.Description)</div>
<div class="card-body px-0 pt-0 pb-0">
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-6">
<table class="table table-light mb-0">
<tbody>
<tr>
<td class="px-3 h2 text-muted">Request amount:</td>
<td class="px-3 h2 text-nowrap text-right">@Model.AmountFormatted</td>
</tr>
<tr>
<td class="px-3 h2 text-muted">Paid so far:</td>
<td class="px-3 h2 text-nowrap text-right">@Model.AmountCollectedFormatted</td>
</tr>
<tr>
<td class="px-3 h2 text-muted">Amount due:</td>
<td class="px-3 h2 text-nowrap text-right">@Model.AmountDueFormatted</td>
</tr>
</tbody>
</table>
@if (Model.Description != null && Model.Description != "" && Model.Description != "<br>")
{
<div class="w-100 px-3 pt-4 pb-3">@Safe.Raw(Model.Description)</div>
}
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="col-sm-12 col-md-12 col-lg-6 pt-2">
<div class="table-responsive">
<table class="table border-top-0 ">
<table class="table border-top-0">
<thead>
<tr>
<th class=" border-top-0" scope="col">Invoice #</th>
<th class=" border-top-0">Price</th>
<th class=" border-top-0">Expiry</th>
<th class=" border-top-0">Status</th>
<th class="border-top-0" scope="col">Invoice #</th>
<th class="border-top-0">Price</th>
<th class="border-top-0">Expiry</th>
<th class="border-top-0">Status</th>
</tr>
</thead>
<tbody>

View File

@ -52,7 +52,7 @@ else
<div class="row w-100 p-0 m-0" style="height: 100vh">
<div class="mx-auto my-auto w-100">
<div class="card">
<h1 class="card-header">
<h1 class="card-header px-3">
{{srvModel.title}}
<span class="text-muted float-right text-center">
@ -64,41 +64,40 @@ else
</template>
</span>
</h1>
<div class="card-body px-0 pt-0">
<div class="row mb-4">
<div class="col-sm-12 col-md-12 col-lg-6 ">
<ul class="w-100 list-group list-group-flush">
<li class="list-group-item list-group-item-light">
<div class="d-flex justify-content-between">
<span class="h2 text-muted">Request amount:</span>
<span class="h2">{{srvModel.amountFormatted}}</span>
</div>
</li>
<li class="list-group-item list-group-item-light">
<div class="d-flex justify-content-between">
<span class="h2 text-muted">Paid so far:</span>
<span class="h2">{{srvModel.amountCollectedFormatted}}</span>
</div>
</li>
<li class="list-group-item list-group-item-light">
<div class="d-flex justify-content-between">
<span class="h2 text-muted">Amount due:</span>
<span class="h2">{{srvModel.amountDueFormatted}}</span>
</div>
</li>
</ul>
<div v-html="srvModel.description" class="w-100 p-2"></div>
<div class="card-body px-0 pt-0 pb-0">
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-6">
<table class="table table-light mb-0">
<tbody>
<tr>
<td class="px-3 h2 text-muted">Request amount:</td>
<td class="px-3 h2 text-nowrap text-right">{{srvModel.amountFormatted}}</td>
</tr>
<tr>
<td class="px-3 h2 text-muted">Paid so far:</td>
<td class="px-3 h2 text-nowrap text-right">{{srvModel.amountCollectedFormatted}}</td>
</tr>
<tr>
<td class="px-3 h2 text-muted">Amount due:</td>
<td class="px-3 h2 text-nowrap text-right">{{srvModel.amountDueFormatted}}</td>
</tr>
</tbody>
</table>
<div
v-if="srvModel.description && srvModel.description !== '' && srvModel.description !== '<br>'"
v-html="srvModel.description"
class="w-100 px-3 pt-4 pb-3"
></div>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="col-sm-12 col-md-12 col-lg-6 pt-2">
<div class="table-responsive">
<table class="table border-top-0 ">
<thead>
<tr>
<th class=" border-top-0" scope="col">Invoice #</th>
<th class=" border-top-0">Price</th>
<th class=" border-top-0">Expiry</th>
<th class=" border-top-0">Status</th>
<th class="border-top-0" scope="col">Invoice #</th>
<th class="border-top-0">Price</th>
<th class="border-top-0">Expiry</th>
<th class="border-top-0">Status</th>
</tr>
</thead>
<tbody>

View File

@ -18,6 +18,7 @@
<div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
<a class="dropdown-item" href="" data-Server="smtp.gmail.com" data-Port="587" data-EnableSSL="true">Gmail.com</a>
<a class="dropdown-item" href="" data-Server="mail.yahoo.com" data-Port="587" data-EnableSSL="true">Yahoo.com</a>
<a class="dropdown-item" href="" data-Server="smtp.office365.com" data-Port="587" data-EnableSSL="true">Office365</a>
</div>
</div>
<p></p>

View File

@ -0,0 +1,56 @@
<script id="VaultConnection" type="text/template">
<div class="vault-feedback vault-feedback1">
<span class="vault-feedback-icon"></span> <span class="vault-feedback-content"></span>
</div>
<div class="vault-feedback vault-feedback2">
<span class="vault-feedback-icon"></span> <span class="vault-feedback-content"></span>
</div>
<div class="vault-feedback vault-feedback3">
<span class="vault-feedback-icon"></span> <span class="vault-feedback-content"></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 class="input-group-append">
<div id="pin-display-delete" class="input-group-text" style="cursor: pointer"><span class="fa fa-remove"></span></div>
</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">
<div class="input-group">
<div class="input-group-prepend">
<label for="Password" class="input-group-text"><span class="input-group-addon fa fa-lock"></span></label>
</div>
<input id="Password" class="form-control" placeholder="Password" />
</div>
</div>
<div class="form-group">
<div class="input-group">
<div class="input-group-prepend">
<label for="PasswordConfirmation" class="input-group-text"><span class="input-group-addon fa fa-lock"></span></label>
</div>
<input id="PasswordConfirmation" class="form-control" placeholder="Passphrase confirmation" />
</div>
</div>
</div>
</script>

View File

@ -9,6 +9,14 @@
.hw-fields {
display: none;
}
.pin-button {
height: 135px;
margin-top: 20px;
background: white;
border: solid lightgray 4px;
cursor: pointer;
}
</style>
}
@ -45,11 +53,14 @@
<div class="dropdown mt-2 text-right">
<div class="btn-group">
<button class="btn btn-link dropdown-toggle" type="button" id="hardwarewlletimportdropdown" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Import from hardware device
Import from...
</button>
<div class="dropdown-menu dropdown-menu-right w-100" aria-labelledby="hardwarewlletimportdropdown">
<button class="dropdown-item" type="button" data-toggle="modal" data-target="#coldcardimport">Coldcard</button>
<button class="dropdown-item check-for-ledger" data-toggle="modal" data-target="#ledgerimport" type="button">Ledger Wallet</button>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="hardwarewlletimportdropdown">
<button class="dropdown-item" type="button" data-toggle="modal" data-target="#coldcardimport">... Coldcard (air gap)</button>
<button class="dropdown-item check-for-ledger" data-toggle="modal" data-target="#ledgerimport" type="button">... Ledger Wallet</button>
@if (Model.CryptoCode == "BTC") {
<button class="dropdown-item check-for-vault" data-toggle="modal" data-target="#btcpayservervault" type="button">... the vault (preview)</button>
}
</div>
</div>
</div>
@ -162,6 +173,8 @@
@await Html.PartialAsync("_ValidationScriptsPartial")
<script src="~/js/ledgerwebsocket.js" type="text/javascript" defer="defer"></script>
<script src="~/js/StoreAddDerivationScheme.js" type="text/javascript" defer="defer"></script>
<script src="~/js/vaultbridge.js" type="text/javascript" defer="defer"></script>
<script src="~/js/vaultbridge.ui.js" type="text/javascript" defer="defer"></script>
<script>
window.coinName = "@Model.Network.DisplayName.ToLowerInvariant()";
</script>

View File

@ -28,10 +28,9 @@
<p>
<a href="https://docs.btcpayserver.org/getting-started/connectwallet/ledgerwallet#manual-setup"
title="Open Ledger wallet manual setup docs"
target="_blank"
rel="noopener noreferrer"
>
title="Open Ledger wallet manual setup docs"
target="_blank"
rel="noopener noreferrer">
Can't find your account in the select?
</a>
</p>
@ -80,3 +79,41 @@
</form>
</div>
</div>
<div id="WebsocketPath" style="display:none;">@Url.Action("VaultBridgeConnection", "Vault", new { cryptoCode = Model.CryptoCode })</div>
<div class="modal fade" id="btcpayservervault" tabindex="-1" role="dialog" aria-labelledby="btcpayservervault" aria-hidden="true">
<div class="modal-dialog" role="document">
<form class="modal-content" form method="post" enctype="multipart/form-data">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">Import from BTCPayServer Vault</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>You may import from BTCPayServer Vault.</p>
<div id="vaultPlaceholder"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<div id="vault-dropdown" style="display:none;" class="dropdown">
<button class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Select the type of address you want
</button>
<div class="dropdown-menu overflow-auto" style="max-height: 200px;">
<a class="dropdown-item" href="#" id="vault-segwit">Segwit (Recommended, cheapest transaction fee)</a>
<a class="dropdown-item" href="#" id="vault-segwitWrapped">Segwit wrapped (less cheap but compatible with old wallets)</a>
<a class="dropdown-item" href="#" id="vault-legacy">Legacy (Not recommended)</a>
</div>
</div>
<button id="vault-confirm" class="btn btn-primary" style="display:none;" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Confirm pin code
</button>
</div>
</form>
</div>
</div>
<partial name="VaultElements" />

View File

@ -29,6 +29,7 @@
<h3>Decoded PSBT</h3>
<div class="form-group">
<form method="post" asp-action="WalletPSBT">
<input type="hidden" asp-for="CryptoCode" />
<input type="hidden" asp-for="PSBT" />
<input type="hidden" asp-for="FileName" />
<div class="dropdown d-inline-block" style="margin-top:16px;">
@ -39,6 +40,9 @@
<button name="command" type="submit" class="dropdown-item" value="ledger">... your Ledger Wallet device</button>
<button name="command" type="submit" class="dropdown-item" value="seed">... an HD private key or mnemonic seed</button>
<button name="command" type="submit" class="dropdown-item" value="save-psbt">... a wallet supporting PSBT (save as file)</button>
@if (Model.CryptoCode == "BTC") {
<button name="command" type="submit" class="dropdown-item" value="vault">... the vault (preview)</button>
}
</div>
</div>
<div class="dropdown d-inline-block">

View File

@ -152,6 +152,9 @@
<button name="command" type="submit" class="dropdown-item" value="ledger">... your Ledger Wallet device</button>
<button name="command" type="submit" class="dropdown-item" value="seed">... an HD private key or mnemonic seed</button>
<button name="command" type="submit" class="dropdown-item" value="analyze-psbt">... a wallet supporting PSBT</button>
@if (Model.CryptoCode == "BTC") {
<button name="command" type="submit" class="dropdown-item" value="vault">... the vault (preview)</button>
}
</div>
</div>
<button type="submit" name="command" value="add-output" class="ml-1 btn btn-secondary">Add another destination </button>

View File

@ -0,0 +1,56 @@
@model WalletSendVaultModel
@{
Layout = "../Shared/_NavLayout.cshtml";
ViewData["Title"] = "Manage wallet";
ViewData.SetActivePageAndTitle(WalletsNavPages.Send);
}
<h4>Sign the transaction with BTCPayServer Vault</h4>
<div id="walletAlert" class="alert alert-danger alert-dismissible" style="display:none;" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<span id="alertMessage"></span>
</div>
<div class="row">
<div id="body" class="col-md-10">
<form id="broadcastForm" asp-action="WalletPSBTReady" method="post" style="display:none;">
<input type="hidden" id="WalletId" asp-for="WalletId" />
<input type="hidden" id="PSBT" asp-for="PSBT" />
<input type="hidden" asp-for="WebsocketPath" />
</form>
<div id="vaultPlaceholder"></div>
</div>
</div>
<partial name="VaultElements" />
@section Scripts
{
<script src="~/js/vaultbridge.js" type="text/javascript" defer="defer"></script>
<script src="~/js/vaultbridge.ui.js" type="text/javascript" defer="defer"></script>
<script type="text/javascript">
$(function () {
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);
vaultUI.askSignPSBT({
walletId: $("#WalletId").val(),
psbt: $("#PSBT").val()
}).then(function (ok) {
if (ok) {
$("#PSBT").val(vaultUI.psbt);
$("#broadcastForm").submit();
}
});
});
</script>
}

View File

@ -68,8 +68,16 @@
</div>
}
}
<div class="form-group">
<button name="command" type="submit" class="btn btn-primary">Save</button>
<div class="form-group d-flex mt-2">
<button name="command" type="submit" class="btn btn-primary" value="save">Save</button>
<div class="dropdown">
<button class="ml-1 btn btn-secondary dropdown-toggle" type="button" id="SendMenu" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Other actions...
</button>
<div class="dropdown-menu" aria-labelledby="SendMenu">
<button name="command" type="submit" class="dropdown-item" value="prune">Prune old transactions from history</button>
</div>
</div>
</div>
</form>
</div>

View File

@ -50,7 +50,7 @@
@if (TempData.HasStatusMessage())
{
<div class="row">
<div class="col-md-10 text-center">
<div class="col-md-12 text-center">
<partial name="_StatusMessage" />
</div>
</div>

View File

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

View File

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
viewBox="0 0 194.21875 193.97749"
height="193.97749"
width="194.21875"
xml:space="preserve"
version="1.1"
id="svg2"
inkscape:version="0.91 r13725"
sodipodi:docname="logo1_S.svg"><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1126"
id="namedview11"
showgrid="false"
inkscape:zoom="2.4679739"
inkscape:cx="81.861935"
inkscape:cy="123.41217"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="1"
inkscape:current-layer="g12"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0" /><metadata
id="metadata8"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs6"><clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath3362"><rect
style="fill:#0000ff;fill-rule:evenodd;stroke:#000000;stroke-width:0.80000001px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
id="rect3364"
width="157.2384"
height="157.69681"
x="318.1441"
y="-385.07358"
ry="1.3752627"
transform="scale(1,-1)" /></clipPath></defs><g
id="g12"
transform="matrix(0.125,0,0,-0.125,-397.89125,479.48875)"><g
id="g3409"><path
id="path16"
style="fill:#004581;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 3185.89,2995.8 c -1.77,21.49 -2.76,43.2 -2.76,65.16 0,411.03 319.09,747.36 723.13,774.95 l -618.54,-641.7 c -54.62,-56.68 -88.55,-126.08 -101.83,-198.41"
inkscape:connector-curvature="0" /><path
id="path18"
style="fill:#004581;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 3960,2284.09 c -270.37,0 -508.4,138.15 -647.57,347.65 l 23.25,-22.42 c 76.82,-74.06 176.93,-109.95 276.2,-108.13 99,1.77 197.53,41.2 271.5,117.59 l -177.95,171.52 c -26.66,-27.31 -62.22,-41.38 -98.02,-42.14 -36.12,-0.65 -72.43,12.41 -100.16,39.15 l -37.98,36.6 c -27.69,26.66 -42.04,62.45 -42.7,98.57 -0.65,36.07 12.36,72.48 39.11,100.21 l 745.68,773.56 c 305.71,-104.45 525.52,-394.17 525.52,-735.29 0,-29.89 -1.73,-59.34 -5.04,-88.32 -19.44,54.57 -51.41,105.56 -95.79,148.35 l -37.93,36.58 c -76.86,74.07 -176.93,110.05 -276.16,108.18 -99.32,-1.77 -198.13,-41.38 -272.19,-118.25 l -290.74,-301.59 177.95,-171.53 290.74,301.61 c 26.71,27.73 62.64,42.04 98.72,42.74 36.12,0.69 72.38,-12.35 100.16,-39.1 l 37.89,-36.59 c 27.69,-26.66 42.09,-62.45 42.74,-98.58 0.61,-36.03 -12.4,-72.48 -39.1,-100.21 L 4027.4,2287.02 c -22.23,-1.9 -44.69,-2.93 -67.4,-2.93"
inkscape:connector-curvature="0" /><path
id="path20"
style="fill:#3384b9;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 4376.22,2292.8 360.66,0 0,433.41 c -17.35,-55.88 -47.59,-108.64 -90.81,-153.48 L 4376.22,2292.8"
inkscape:connector-curvature="0" /></g></g></svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@ -1,5 +1,4 @@
function initLedger(){
function initLedger() {
var ledgerDetected = false;
var loc = window.location, new_uri;
@ -90,12 +89,57 @@
$(document).ready(function () {
var ledgerInit = false;
$(".check-for-ledger").on("click", function(){
if(!ledgerInit){
$(".check-for-ledger").on("click", function () {
if (!ledgerInit) {
initLedger();
}
ledgerInit = true;
});
function show(id, category) {
$("." + category).css("display", "none");
$("#" + id).css("display", "block");
}
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;
function displayXPubs(xpubs) {
$("#vault-dropdown").css("display", "block");
$("#vault-dropdown .dropdown-item").click(function () {
var id = $(this).attr('id').replace("vault-", "");
var xpub = xpubs[id];
$("#DerivationScheme").val(xpub.strategy);
$("#RootFingerprint").val(xpubs.fingerprint);
$("#AccountKey").val(xpub.accountKey);
$("#Source").val("Vault");
$("#DerivationSchemeFormat").val("BTCPay");
$("#KeyPath").val(xpub.keyPath);
$(".modal").modal('hide');
$(".hw-fields").show();
});
}
var vaultInit = false;
$(".check-for-vault").on("click", async function () {
if (vaultInit)
return;
vaultInit = true;
var html = $("#VaultConnection").html();
$("#vaultPlaceholder").html(html);
var vaultUI = new vaultui.VaultBridgeUI(ws_uri);
if (await vaultUI.askForXPubs()) {
displayXPubs(vaultUI.xpubs);
}
});
});

View File

@ -0,0 +1,104 @@
var vault = (function () {
/** @param {WebSocket} websocket
*/
function VaultBridge(websocket) {
var self = this;
/**
* @type {WebSocket}
*/
this.socket = websocket;
this.onerror = function (error) { };
this.onbackendmessage = function (json) { };
/**
* @returns {Promise}
*/
this.waitBackendMessage = function () {
return new Promise(function (resolve, reject) {
self.nextResolveBackendMessage = resolve;
});
};
this.socket.onmessage = function (event) {
if (typeof event.data === "string") {
var jsonObject = JSON.parse(event.data);
if (jsonObject.hasOwnProperty("params")) {
var request = new XMLHttpRequest();
request.onreadystatechange = function () {
if (request.readyState == 4 && request.status == 200) {
self.socket.send(request.responseText);
}
if (request.readyState == 4 && request.status == 0) {
self.onerror(vault.errors.notRunning);
}
if (request.readyState == 4 && request.status == 401) {
self.onerror(vault.errors.denied);
}
};
request.open('POST', 'http://localhost:65092/hwi-bridge/v1');
request.send(JSON.stringify(jsonObject));
}
else {
self.onbackendmessage(jsonObject);
if (self.nextResolveBackendMessage)
self.nextResolveBackendMessage(jsonObject);
}
}
};
}
/**
* @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.open('GET', 'http://localhost:65092/hwi-bridge/v1/request-permission');
request.send();
});
}
return {
errors: {
notRunning: "NotRunning",
denied: "Denied",
socketNotSupported: "SocketNotSupported",
socketError: "SocketError",
},
askVaultPermission: askVaultPermission,
connectToBackendSocket: connectToBackendSocket,
VaultBridge: VaultBridge
};
})();

View File

@ -0,0 +1,286 @@
/// <reference path="vaultbridge.js" />
/// file: vaultbridge.js
var vaultui = (function () {
/**
* @param {string} type
* @param {string} txt
* @param {string} category
* @param {string} id
*/
function VaultFeedback(type, txt, category, id) {
var self = this;
this.type = type;
this.txt = txt;
this.category = category;
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.category, self.id);
};
}
var VaultFeedbacks = {
vaultLoading: new VaultFeedback("?", "Checking BTCPayServer Vault is running...", "vault-feedback1", "vault-loading"),
vaultDenied: new VaultFeedback("failed", "The user declined access to the vault.", "vault-feedback1", "vault-denied"),
vaultGranted: new VaultFeedback("ok", "Access to vault granted by owner.", "vault-feedback1", "vault-granted"),
noVault: new VaultFeedback("failed", "BTCPayServer Vault does not seems running, you can download it on <a target=\"_blank\" href=\"https://github.com/btcpayserver/BTCPayServer.Vault/releases/latest\">Github</a>.", "vault-feedback1", "no-vault"),
noWebsockets: new VaultFeedback("failed", "Web sockets are not supported by the browser.", "vault-feedback1", "no-websocket"),
errorWebsockets: new VaultFeedback("failed", "Error of the websocket while connecting to the backend.", "vault-feedback1", "error-websocket"),
bridgeConnected: new VaultFeedback("ok", "BTCPayServer successfully connected to the vault.", "vault-feedback1", "bridge-connected"),
noDevice: new VaultFeedback("failed", "No device connected.", "vault-feedback2", "no-device"),
fetchingDevice: new VaultFeedback("?", "Fetching device...", "vault-feedback2", "fetching-device"),
deviceFound: new VaultFeedback("ok", "Device found: {{0}}", "vault-feedback2", "device-selected"),
fetchingXpubs: new VaultFeedback("?", "Fetching public keys...", "vault-feedback3", "fetching-xpubs"),
fetchedXpubs: new VaultFeedback("ok", "Public keys successfully fetched.", "vault-feedback3", "xpubs-fetched"),
unexpectedError: new VaultFeedback("failed", "An unexpected error happened.", "vault-feedback3", "unknown-error"),
needPin: new VaultFeedback("?", "Enter the pin.", "vault-feedback3", "need-pin"),
incorrectPin: new VaultFeedback("failed", "Incorrect pin code.", "vault-feedback3", "incorrect-pin"),
invalidPasswordConfirmation: new VaultFeedback("failed", "Invalid password confirmation.", "vault-feedback3", "invalid-password-confirm"),
wrongWallet: new VaultFeedback("failed", "This device can't sign the transaction.", "vault-feedback3", "wrong-wallet"),
needPassphrase: new VaultFeedback("?", "Enter the passphrase.", "vault-feedback3", "need-passphrase"),
signingTransaction: new VaultFeedback("?", "Signing the transaction...", "vault-feedback3", "ask-signing"),
signingRejected: new VaultFeedback("failed", "The user refused to sign the transaction", "vault-feedback3", "user-reject"),
};
/**
* @param {string} backend_uri
*/
function VaultBridgeUI(backend_uri) {
/**
* @type {VaultBridgeUI}
*/
var self = this;
this.backend_uri = backend_uri;
/**
* @type {vault.VaultBridge}
*/
this.bridge = null;
/**
* @type {string}
*/
this.psbt = null;
this.xpubs = null;
/**
* @param {VaultFeedback} feedback
*/
function show(feedback) {
var icon = $(".vault-feedback." + feedback.category + " " + ".vault-feedback-icon");
icon.removeClass();
icon.addClass("vault-feedback-icon");
if (feedback.type == "?") {
icon.addClass("fa fa-question-circle feedback-icon-loading");
}
else if (feedback.type == "ok") {
icon.addClass("fa fa-check-circle feedback-icon-success");
}
else if (feedback.type == "failed") {
icon.addClass("fa fa-times-circle feedback-icon-failed");
}
var content = $(".vault-feedback." + feedback.category + " " + ".vault-feedback-content");
content.html(feedback.txt);
}
function showError(json) {
if (json.hasOwnProperty("error")) {
for (var key in VaultFeedbacks) {
if (VaultFeedbacks.hasOwnProperty(key) && VaultFeedbacks[key].id == json.error) {
show(VaultFeedbacks[key]);
if (json.hasOwnProperty("details"))
console.warn(json.details);
return;
}
}
show(VaultFeedbacks.unexpectedError);
if (json.hasOwnProperty("details"))
console.warn(json.details);
}
}
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 (!handled) {
showError(json);
}
}
return false;
}
this.ensureConnectedToBackend = async function () {
if (!self.bridge) {
$("#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.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;
show(VaultFeedbacks.fetchingXpubs);
self.bridge.socket.send("ask-xpubs");
var json = await self.bridge.waitBackendMessage();
if (json.hasOwnProperty("error")) {
if (await needRetry(json))
return await self.askForXPubs();
return false;
}
show(VaultFeedbacks.fetchedXpubs);
self.xpubs = json;
return true;
};
/**
* @returns {Promise<string>}
*/
this.getUserEnterPin = function () {
show(VaultFeedbacks.needPin);
$("#pin-input").css("display", "block");
$("#vault-confirm").css("display", "block");
return new Promise(function (resolve, reject) {
var pinCode = "";
$("#vault-confirm").click(async function () {
$("#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");
return new Promise(function (resolve, reject) {
$("#vault-confirm").click(async function () {
var passphrase = $("#Password").val();
if (passphrase !== $("#PasswordConfirmation").val()) {
show("invalid-password-confirm");
return;
}
$("#passphrase-input").css("display", "none");
$("#vault-confirm").css("display", "none");
$(this).unbind();
resolve(passphrase);
});
});
};
/**
* @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 (await needRetry(json))
return await self.askForPin();
return false;
}
var pinCode = await self.getUserEnterPin();
var passphrase = await self.getUserPassphrase();
self.bridge.socket.send(JSON.stringify({ pinCode: pinCode, passphrase: passphrase }));
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;
};
}
return {
VaultFeedback: VaultFeedback,
VaultBridgeUI: VaultBridgeUI
};
})();

View File

@ -23,7 +23,7 @@
"Address": "Indirizzo",
"Copied": "Copiato",
"ConversionTab_BodyTop": "Puoi pagare {{btcDue}} {{cryptoCode}} usando altcoin diverse da quelle che il commerciante supporta direttamente.",
"ConversionTab_BodyDesc": "Questo servizio è fornito da terze parti. Ricorda che non abbiamo alcun controllo su come tali parti inoltreranno i fondi. La fattura verrà contrassegnata come pagata solo dopo aver ricevuto i fondi su {{cryptoCode}} Blockchain.",
"ConversionTab_BodyDesc": "Questo servizio è fornito da parti. Ricorda che non abbiamo alcun controllo su come tali parti inoltreranno i tuoi fondi. La fattura verrà contrassegnata come pagata solo dopo aver ricevuto i fondi sulla {{cryptoCode}} Blockchain.",
"ConversionTab_CalculateAmount_Error": "Riprova",
"ConversionTab_LoadCurrencies_Error": "Riprova",
"ConversionTab_Lightning": "Nessun fornitore di conversione disponibile per i pagamenti Lightning Network.",

View File

@ -122,3 +122,17 @@ a.nav-link:hover {
background: white;
display: inline-block;
}
pre {
color: var(--btcpay-preformatted-text-color);
}
.feedback-icon-loading {
color: orange;
}
.feedback-icon-success {
color: green;
}
.feedback-icon-failed {
color: red;
}

View File

@ -62,6 +62,8 @@
--btcpay-header-bg: var(--btcpay-brand-darker);
--btcpay-footer-bg: var(--btcpay-brand-darkest);
--btcpay-preformatted-text-color: var(--btcpay-color-white);
--btcpay-font-size-base: 16px;
--btcpay-font-family-head: 'Open Sans', 'Helvetica Neue', Arial, sans-serif;
--btcpay-font-family-base: -apple-system, 'Open Sans', BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";

View File

@ -57,6 +57,8 @@
--btcpay-bg-nav-link-active: #d9f7ef;
--btcpay-preformatted-text-color: var(--btcpay-color-neutral-900);
--btcpay-font-size-base: 14px;
--btcpay-font-family-head: 'Open Sans', 'Helvetica Neue', Arial, sans-serif;
--btcpay-font-family-base: 'Helvetica Neue', Arial, sans-serif;

View File

@ -48,6 +48,8 @@
--btcpay-section-heading-text-align: left;
--btcpay-preformatted-text-color: var(--btcpay-color-neutral-900);
--btcpay-font-size-base: 16px;
--btcpay-font-family-head: 'Open Sans', 'Helvetica Neue', Arial, sans-serif;
--btcpay-font-family-base: -apple-system, 'Open Sans', BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";

View File

@ -1,5 +1,5 @@
<Project>
<PropertyGroup>
<Version>1.0.3.138</Version>
<Version>1.0.3.142</Version>
</PropertyGroup>
</Project>

View File

@ -165,6 +165,20 @@ The BTCPay Server Project is proudly supported by these entities through the [BT
<span>ACINQ</span>
</a>
</td>
<td align="center" valign="middle">
<a href="https://lunanode.com" target="_blank">
<img src="BTCPayServer/wwwroot/img/lunanode.svg" alt="LunaNode" height=100>
<br/>
<span>LunaNode</span>
</a>
</td>
<td align="center" valign="middle">
<a href="https://walletofsatoshi.com/" target="_blank">
<img src="BTCPayServer/wwwroot/img/walletofsatoshi.svg" alt="Wallet of Satoshi" height=100>
<br/>
<span>Wallet of Satoshi</span>
</a>
</td>
</tr>
</tbody>
</table>