Compare commits
99 Commits
v1.0.3.138
...
v1.0.3.146
Author | SHA1 | Date | |
---|---|---|---|
33703b83a3 | |||
23b9dfed2c | |||
b8f6ef8844 | |||
6f6b4c8ead | |||
5d87dd5861 | |||
02c8bf4469 | |||
540cb912f3 | |||
93f490f570 | |||
de2e222ae5 | |||
12055a000b | |||
6addb3e481 | |||
d9cd916440 | |||
452a705b75 | |||
1c8206c749 | |||
062c42e524 | |||
2d932ebb21 | |||
0e0fa53517 | |||
4e20730379 | |||
1d70d935b8 | |||
9b8f42cdf6 | |||
eb85b1a7b4 | |||
df7e2073df | |||
84d943d6cc | |||
a1d82b0e8b | |||
ab7c124302 | |||
544597348b | |||
1559c510be | |||
3e08581e9b | |||
49c70d9b04 | |||
50e7d8389c | |||
ea5bd6d435 | |||
cbb37674e3 | |||
e70d5ceb08 | |||
5d7b253edd | |||
df38b84bbb | |||
a3fc75ea3b | |||
71a8166027 | |||
564dd95d81 | |||
eb16b435df | |||
f94daed06d | |||
e841bfa148 | |||
03b76380e7 | |||
c6671f7122 | |||
db56c2b106 | |||
f7b2c836b7 | |||
6ec379b538 | |||
d6e1d34ebf | |||
bc2a039ea2 | |||
e31e600144 | |||
91f83d751f | |||
b8288f1efa | |||
fa61c2bcdd | |||
2d168e1d9b | |||
3e7ad4a4f6 | |||
d2357ee8b9 | |||
9f04c7d0bc | |||
9baa7eed80 | |||
287fbc523f | |||
ea6df35c87 | |||
2e0db1a430 | |||
7ccf0c3612 | |||
1b58058796 | |||
98276bcb3d | |||
1aa4dbb90d | |||
eef623fb4b | |||
84bbbcbe10 | |||
a324e2aeaf | |||
d6b3530384 | |||
dfe8a10e1a | |||
5afa847e6e | |||
1bcbc9bab0 | |||
b915544798 | |||
2b9f390c64 | |||
ee42d5c7b4 | |||
f809dd51a6 | |||
1a8d6e5c05 | |||
869ba745b2 | |||
180dfb6edf | |||
45b08ac8d2 | |||
9a4b385432 | |||
08289b89c5 | |||
a31d1d81c8 | |||
e4c7bb0378 | |||
374aaf2e2b | |||
52fd686993 | |||
03c36ef0d2 | |||
71a6ffac2e | |||
92777ba181 | |||
81843fb609 | |||
3af3ffd038 | |||
2ce5cd0b6f | |||
d791fd59e9 | |||
6064e3ce55 | |||
0fcfe0e977 | |||
997df5c64d | |||
27af96662f | |||
0be6f3ca70 | |||
7af80611b6 | |||
929b5c7951 |
@ -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)
|
||||
|
@ -3,8 +3,8 @@
|
||||
<Import Project="../Build/Common.csproj" />
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.9" Condition="'$(TargetFramework)' == 'netcoreapp2.1'" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.14" Condition="'$(TargetFramework)' == 'netcoreapp2.1'" />
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" Condition="'$(TargetFramework)' != 'netcoreapp2.1'" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="2.0.0.26" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="3.0.1" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
@ -2,9 +2,9 @@
|
||||
<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.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="2.1.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.App" AllowExplicitVersion="true" Version="2.1.14" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.14" />
|
||||
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="2.1.4" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="2.1.2" />
|
||||
<PackageReference Include="OpenIddict.EntityFrameworkCore" Version="3.0.0-alpha1.19515.63" />
|
||||
</ItemGroup>
|
||||
|
@ -7,7 +7,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.9" Condition="'$(TargetFramework)' == 'netcoreapp2.1'" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.14" Condition="'$(TargetFramework)' == 'netcoreapp2.1'" />
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" Condition="'$(TargetFramework)' != 'netcoreapp2.1'" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.3.1" Condition="'$(TargetFramework)' != 'netcoreapp2.1'" />
|
||||
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.7" Condition="'$(TargetFramework)' != 'netcoreapp2.1'" />
|
||||
|
@ -1,4 +1,4 @@
|
||||
FROM mcr.microsoft.com/dotnet/core/sdk:2.1.505-alpine3.7 AS builder
|
||||
FROM mcr.microsoft.com/dotnet/core/sdk:2.1.607-alpine3.9 AS builder
|
||||
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT false
|
||||
RUN apk add --no-cache icu-libs
|
||||
ENV LC_ALL en_US.UTF-8
|
||||
|
@ -76,7 +76,7 @@ services:
|
||||
- customer_lnd
|
||||
- merchant_lnd
|
||||
nbxplorer:
|
||||
image: nicolasdorier/nbxplorer:2.0.0.66
|
||||
image: nicolasdorier/nbxplorer:2.1.2
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "32838:32838"
|
||||
|
@ -27,12 +27,13 @@
|
||||
<EmbeddedResource Include="Currencies.txt" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BTCPayServer.Hwi" Version="1.1.3" />
|
||||
<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.14" 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>
|
||||
|
@ -294,6 +294,10 @@ namespace BTCPayServer.Controllers
|
||||
};
|
||||
|
||||
paymentMethodHandler.PreparePaymentModel(model, dto, storeBlob);
|
||||
if (model.IsLightning && storeBlob.LightningAmountInSatoshi && model.CryptoCode == "Sats")
|
||||
{
|
||||
model.Rate = _CurrencyNameTable.DisplayFormatCurrency(paymentMethod.Rate / 100_000_000, paymentMethod.ParentEntity.ProductInformation.Currency);
|
||||
}
|
||||
model.UISettings = paymentMethodHandler.GetCheckoutUISettings();
|
||||
model.PaymentMethodId = paymentMethodId.ToString();
|
||||
var expiration = TimeSpan.FromSeconds(model.ExpirationSeconds);
|
||||
|
@ -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))
|
||||
{
|
||||
@ -338,8 +337,10 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var keyPath = deposit.GetKeyPath((uint)i);
|
||||
var rootedKeyPath = vm.GetAccountKeypath()?.Derive(keyPath);
|
||||
var address = line.Derive((uint)i);
|
||||
vm.AddressSamples.Add((deposit.GetKeyPath((uint)i).ToString(), address.ScriptPubKey.GetDestinationAddress(strategy.Network.NBitcoinNetwork).ToString()));
|
||||
vm.AddressSamples.Add((keyPath.ToString(), address.ScriptPubKey.GetDestinationAddress(strategy.Network.NBitcoinNetwork).ToString(), rootedKeyPath));
|
||||
}
|
||||
}
|
||||
vm.Confirmation = true;
|
||||
|
385
BTCPayServer/Controllers/VaultController.cs
Normal file
385
BTCPayServer/Controllers/VaultController.cs
Normal file
@ -0,0 +1,385 @@
|
||||
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.Hwi;
|
||||
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;
|
||||
HwiEnumerateEntry deviceEntry = null;
|
||||
HDFingerprint? fingerprint = null;
|
||||
string password = null;
|
||||
var websocketHelper = new WebSocketHelper(websocket);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
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)
|
||||
{
|
||||
fingerprint = (await device.GetXPubAsync(new KeyPath("44'"), cancellationToken)).ExtPubKey.ParentFingerprint;
|
||||
}
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
try
|
||||
{
|
||||
psbt = await device.SignPSBTAsync(psbt, cancellationToken);
|
||||
}
|
||||
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 "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)
|
||||
{
|
||||
fingerprint = (await device.GetXPubAsync(new KeyPath("44'"), cancellationToken)).ExtPubKey.ParentFingerprint;
|
||||
}
|
||||
result["fingerprint"] = fingerprint.Value.ToString();
|
||||
|
||||
DerivationStrategyBase strategy = null;
|
||||
KeyPath keyPath = null;
|
||||
BitcoinExtPubKey xpub = null;
|
||||
|
||||
if (!network.NBitcoinNetwork.Consensus.SupportSegwit && addressType != "legacy")
|
||||
{
|
||||
await websocketHelper.Send("{ \"error\": \"segwit-notsupported\"}", cancellationToken);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (addressType == "segwit")
|
||||
{
|
||||
keyPath = new KeyPath("84'").Derive(network.CoinType).Derive(accountNumber, true);
|
||||
xpub = await device.GetXPubAsync(keyPath);
|
||||
strategy = factory.CreateDirectDerivationStrategy(xpub, new DerivationStrategyOptions()
|
||||
{
|
||||
ScriptPubKeyType = ScriptPubKeyType.Segwit
|
||||
});
|
||||
}
|
||||
else if (addressType == "segwitWrapped")
|
||||
{
|
||||
keyPath = new KeyPath("49'").Derive(network.CoinType).Derive(accountNumber, true);
|
||||
xpub = await device.GetXPubAsync(keyPath);
|
||||
strategy = factory.CreateDirectDerivationStrategy(xpub, new DerivationStrategyOptions()
|
||||
{
|
||||
ScriptPubKeyType = ScriptPubKeyType.SegwitP2SH
|
||||
});
|
||||
}
|
||||
else if (addressType == "legacy")
|
||||
{
|
||||
keyPath = new KeyPath("44'").Derive(network.CoinType).Derive(accountNumber, true);
|
||||
xpub = await device.GetXPubAsync(keyPath);
|
||||
strategy = factory.CreateDirectDerivationStrategy(xpub, new DerivationStrategyOptions()
|
||||
{
|
||||
ScriptPubKeyType = ScriptPubKeyType.Legacy
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
await websocketHelper.Send("{ \"error\": \"invalid-addresstype\"}", cancellationToken);
|
||||
continue;
|
||||
}
|
||||
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:
|
||||
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;
|
||||
}
|
||||
device = new HwiDeviceClient(hwi, deviceEntry.DeviceSelector, deviceEntry.Model, deviceEntry.Fingerprint);
|
||||
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 (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("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 == HardwareWalletModels.Trezor_T || deviceEntry.Model == HardwareWalletModels.Trezor_T_Simulator);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
@ -55,12 +55,13 @@ namespace BTCPayServer.Controllers
|
||||
WalletId walletId, WalletPSBTViewModel vm)
|
||||
{
|
||||
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode);
|
||||
vm.CryptoCode = network.CryptoCode;
|
||||
if (await vm.GetPSBT(network.NBitcoinNetwork) is PSBT psbt)
|
||||
{
|
||||
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")]
|
||||
@ -72,6 +73,7 @@ namespace BTCPayServer.Controllers
|
||||
if (command == null)
|
||||
return await WalletPSBT(walletId, vm);
|
||||
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode);
|
||||
vm.CryptoCode = network.CryptoCode;
|
||||
var psbt = await vm.GetPSBT(network.NBitcoinNetwork);
|
||||
if (psbt == null)
|
||||
{
|
||||
@ -88,6 +90,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 +160,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
|
||||
|
@ -252,7 +252,7 @@ namespace BTCPayServer.Controllers
|
||||
vm.Id = tx.TransactionId.ToString();
|
||||
vm.Link = string.Format(CultureInfo.InvariantCulture, paymentMethod.Network.BlockExplorerLink, vm.Id);
|
||||
vm.Timestamp = tx.Timestamp;
|
||||
vm.Positive = tx.BalanceChange >= Money.Zero;
|
||||
vm.Positive = tx.BalanceChange.GetValue(wallet.Network) >= 0;
|
||||
vm.Balance = tx.BalanceChange.ToString();
|
||||
vm.IsConfirmed = tx.Confirmations != 0;
|
||||
|
||||
@ -313,7 +313,7 @@ namespace BTCPayServer.Controllers
|
||||
var feeProvider = _feeRateProvider.CreateFeeProvider(network);
|
||||
var recommendedFees = feeProvider.GetFeeRateAsync();
|
||||
var balance = _walletProvider.GetWallet(network).GetBalance(paymentMethod.AccountDerivation);
|
||||
model.CurrentBalance = (await balance).ToDecimal(MoneyUnit.BTC);
|
||||
model.CurrentBalance = await balance;
|
||||
model.RecommendedSatoshiPerByte = (int)(await recommendedFees).GetFee(1).Satoshi;
|
||||
model.FeeSatoshiPerByte = model.RecommendedSatoshiPerByte;
|
||||
model.SupportRBF = network.SupportRBF;
|
||||
@ -387,9 +387,20 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
subtractFeesOutputsCount.Add(i);
|
||||
}
|
||||
var destination = ParseDestination(transactionOutput.DestinationAddress, network.NBitcoinNetwork);
|
||||
if (destination == null)
|
||||
ModelState.AddModelError(nameof(transactionOutput.DestinationAddress), "Invalid address");
|
||||
transactionOutput.DestinationAddress = transactionOutput.DestinationAddress.Trim();
|
||||
|
||||
try
|
||||
{
|
||||
BitcoinAddress.Create(transactionOutput.DestinationAddress, network.NBitcoinNetwork);
|
||||
}
|
||||
catch
|
||||
{
|
||||
var inputName =
|
||||
string.Format(CultureInfo.InvariantCulture, "Outputs[{0}].", i.ToString(CultureInfo.InvariantCulture)) +
|
||||
nameof(transactionOutput.DestinationAddress);
|
||||
|
||||
ModelState.AddModelError(inputName, "Invalid address");
|
||||
}
|
||||
|
||||
if (transactionOutput.Amount.HasValue)
|
||||
{
|
||||
@ -425,7 +436,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
if (!ModelState.IsValid)
|
||||
return View(vm);
|
||||
|
||||
DerivationSchemeSettings derivationScheme = GetDerivationSchemeSettings(walletId);
|
||||
@ -449,6 +460,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 +476,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()
|
||||
@ -588,19 +611,6 @@ namespace BTCPayServer.Controllers
|
||||
return v.ToString() + " " + network.CryptoCode;
|
||||
}
|
||||
|
||||
private IDestination[] ParseDestination(string destination, Network network)
|
||||
{
|
||||
try
|
||||
{
|
||||
destination = destination?.Trim();
|
||||
return new IDestination[] { BitcoinAddress.Create(destination, network) };
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private IActionResult RedirectToWalletTransaction(WalletId walletId, Transaction transaction)
|
||||
{
|
||||
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode);
|
||||
@ -716,7 +726,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
try
|
||||
{
|
||||
return (await wallet.GetBalance(derivationStrategy, cts.Token)).ToString();
|
||||
return (await wallet.GetBalance(derivationStrategy, cts.Token)).ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
catch
|
||||
{
|
||||
@ -873,27 +883,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, new PruneRequest(), 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));
|
||||
}
|
||||
}
|
||||
|
||||
|
29
BTCPayServer/Extensions/MoneyExtensions.cs
Normal file
29
BTCPayServer/Extensions/MoneyExtensions.cs
Normal file
@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public static class MoneyExtensions
|
||||
{
|
||||
public static decimal GetValue(this IMoney m, BTCPayNetwork network = null)
|
||||
{
|
||||
switch (m)
|
||||
{
|
||||
case Money money:
|
||||
return money.ToDecimal(MoneyUnit.BTC);
|
||||
// case MoneyBag mb:
|
||||
// return mb.Select(money => money.GetValue(network)).Sum();
|
||||
// case AssetMoney assetMoney:
|
||||
// if (network is ElementsBTCPayNetwork elementsBTCPayNetwork)
|
||||
// {
|
||||
// return elementsBTCPayNetwork.AssetId == assetMoney.AssetId
|
||||
// ? new Money(assetMoney.Quantity)
|
||||
// : Money.Zero;
|
||||
// }
|
||||
// throw new NotSupportedException("IMoney type not supported");
|
||||
default:
|
||||
throw new NotSupportedException("IMoney type not supported");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -26,6 +26,11 @@ namespace BTCPayServer.HostedServices
|
||||
else
|
||||
_themeUri = data.ThemeCssUri;
|
||||
|
||||
if (String.IsNullOrWhiteSpace(data.CustomThemeCssUri))
|
||||
_customThemeUri = null;
|
||||
else
|
||||
_customThemeUri = data.CustomThemeCssUri;
|
||||
|
||||
if (String.IsNullOrWhiteSpace(data.BootstrapCssUri))
|
||||
_bootstrapUri = "/main/bootstrap/bootstrap.css";
|
||||
else
|
||||
@ -35,6 +40,7 @@ namespace BTCPayServer.HostedServices
|
||||
_creativeStartUri = "/main/bootstrap4-creativestart/creative.css";
|
||||
else
|
||||
_creativeStartUri = data.CreativeStartCssUri;
|
||||
|
||||
FirstRun = data.FirstRun;
|
||||
}
|
||||
|
||||
@ -44,6 +50,12 @@ namespace BTCPayServer.HostedServices
|
||||
get { return _themeUri; }
|
||||
}
|
||||
|
||||
private string _customThemeUri;
|
||||
public string CustomThemeUri
|
||||
{
|
||||
get { return _customThemeUri; }
|
||||
}
|
||||
|
||||
private string _bootstrapUri;
|
||||
public string BootstrapUri
|
||||
{
|
||||
@ -108,6 +120,10 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
policies.Clear();
|
||||
}
|
||||
if (manager.CustomThemeUri != null && Uri.TryCreate(manager.CustomThemeUri, UriKind.Absolute, out uri))
|
||||
{
|
||||
policies.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
28
BTCPayServer/HwiWebSocketTransport.cs
Normal file
28
BTCPayServer/HwiWebSocketTransport.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -22,7 +22,7 @@ namespace BTCPayServer.Models.AppViewModels
|
||||
[Display(Name = "Callback Notification Url")]
|
||||
[Uri]
|
||||
public string NotificationUrl { get; set; }
|
||||
[Display(Name = "Invoice Email Notification")]
|
||||
[Display(Name = "Invoice IPN Notification")]
|
||||
[EmailAddress]
|
||||
public string NotificationEmail { get; set; }
|
||||
|
||||
|
@ -32,7 +32,7 @@ namespace BTCPayServer.Models.AppViewModels
|
||||
[Display(Name = "Callback Notification Url")]
|
||||
[Uri]
|
||||
public string NotificationUrl { get; set; }
|
||||
[Display(Name = "Invoice Email Notification")]
|
||||
[Display(Name = "Invoice IPN Notification")]
|
||||
[EmailAddress]
|
||||
public string NotificationEmail { get; set; }
|
||||
|
||||
|
@ -78,6 +78,5 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
public string RootPath { get; set; }
|
||||
public decimal CoinSwitchAmountMarkupPercentage { get; set; }
|
||||
public bool RedirectAutomatically { get; set; }
|
||||
public string RateBaseAmount { get; set; } = "1";
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -19,10 +19,10 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
get; set;
|
||||
}
|
||||
|
||||
public List<(string KeyPath, string Address)> AddressSamples
|
||||
public List<(string KeyPath, string Address, RootedKeyPath RootedKeyPath)> AddressSamples
|
||||
{
|
||||
get; set;
|
||||
} = new List<(string KeyPath, string Address)>();
|
||||
} = new List<(string KeyPath, string Address, RootedKeyPath RootedKeyPath)>();
|
||||
|
||||
public string CryptoCode { get; set; }
|
||||
public string KeyPath { get; set; }
|
||||
@ -41,5 +41,16 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
public string DerivationSchemeFormat { get; set; }
|
||||
public string AccountKey { get; set; }
|
||||
public BTCPayNetwork Network { get; set; }
|
||||
|
||||
public RootedKeyPath GetAccountKeypath()
|
||||
{
|
||||
if (KeyPath != null && RootFingerprint != null &&
|
||||
NBitcoin.KeyPath.TryParse(KeyPath, out var p) &&
|
||||
HDFingerprint.TryParse(RootFingerprint, out var fp))
|
||||
{
|
||||
return new RootedKeyPath(fp, p);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -17,8 +17,10 @@ namespace BTCPayServer.Models.WalletViewModels
|
||||
[Required]
|
||||
public string DestinationAddress { get; set; }
|
||||
|
||||
[Display(Name = "Amount")] [Required] [Range(0.0, double.MaxValue)]public decimal? Amount { get; set; }
|
||||
|
||||
[Display(Name = "Amount")]
|
||||
[Required]
|
||||
[Range(1E-08, 21E6)]
|
||||
public decimal? Amount { get; set; }
|
||||
|
||||
[Display(Name = "Subtract fees from amount")]
|
||||
public bool SubtractFeesFromOutput { get; set; }
|
||||
|
14
BTCPayServer/Models/WalletViewModels/WalletSendVaultModel.cs
Normal file
14
BTCPayServer/Models/WalletViewModels/WalletSendVaultModel.cs
Normal 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; }
|
||||
}
|
||||
}
|
@ -20,10 +20,10 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||
{
|
||||
|
||||
}
|
||||
public BitcoinLikePaymentData(Coin coin, bool rbf)
|
||||
public BitcoinLikePaymentData(TxOut txout, OutPoint outpoint, bool rbf)
|
||||
{
|
||||
Outpoint = coin.Outpoint;
|
||||
Output = coin.TxOut;
|
||||
Outpoint = outpoint;
|
||||
Output = txout;
|
||||
ConfirmationCount = 0;
|
||||
RBF = rbf;
|
||||
}
|
||||
|
@ -157,7 +157,7 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||
var invoice = (await _InvoiceRepository.GetInvoicesFromAddresses(new [] {key})).FirstOrDefault();
|
||||
if (invoice != null)
|
||||
{
|
||||
var paymentData = new BitcoinLikePaymentData(txCoin, evt.TransactionData.Transaction.RBF);
|
||||
var paymentData = new BitcoinLikePaymentData(txCoin.TxOut, txCoin.Outpoint, evt.TransactionData.Transaction.RBF);
|
||||
var alreadyExist = GetAllBitcoinPaymentData(invoice).Where(c => c.GetPaymentId() == paymentData.GetPaymentId()).Any();
|
||||
if (!alreadyExist)
|
||||
{
|
||||
@ -338,7 +338,7 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||
foreach (var coin in coins.Where(c => !alreadyAccounted.Contains(c.Coin.Outpoint)))
|
||||
{
|
||||
var transaction = await wallet.GetTransactionAsync(coin.Coin.Outpoint.Hash);
|
||||
var paymentData = new BitcoinLikePaymentData(coin.Coin, transaction.Transaction.RBF);
|
||||
var paymentData = new BitcoinLikePaymentData(coin.Coin.TxOut, coin.Coin.Outpoint, transaction.Transaction.RBF);
|
||||
var payment = await _InvoiceRepository.AddPayment(invoice.Id, coin.Timestamp, paymentData, network).ConfigureAwait(false);
|
||||
alreadyAccounted.Add(coin.Coin.Outpoint);
|
||||
if (payment != null)
|
||||
@ -385,10 +385,13 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||
}
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
leases.Dispose();
|
||||
_Cts.Cancel();
|
||||
await Task.WhenAny(_RunningTask.Task, Task.Delay(-1, cancellationToken));
|
||||
Logs.PayServer.LogInformation($"{this.GetType().Name} successfully exited...");
|
||||
if (_Cts != null)
|
||||
{
|
||||
leases.Dispose();
|
||||
_Cts.Cancel();
|
||||
await Task.WhenAny(_RunningTask.Task, Task.Delay(-1, cancellationToken));
|
||||
Logs.PayServer.LogInformation($"{this.GetType().Name} successfully exited...");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -185,7 +185,6 @@ namespace BTCPayServer.Payments.Lightning
|
||||
model.BtcPaid = Money.Parse(model.BtcPaid).ToUnit(MoneyUnit.Satoshi).ToString(CultureInfo.InvariantCulture);
|
||||
model.NetworkFee = new Money(model.NetworkFee, MoneyUnit.BTC).ToUnit(MoneyUnit.Satoshi);
|
||||
model.OrderAmount = Money.Parse(model.OrderAmount).ToUnit(MoneyUnit.Satoshi).ToString(CultureInfo.InvariantCulture);
|
||||
model.RateBaseAmount = Money.FromUnit(1, MoneyUnit.BTC).Satoshi.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
||||
public override string GetCryptoImage(PaymentMethodId paymentMethodId)
|
||||
|
@ -14,12 +14,18 @@ namespace BTCPayServer.Services
|
||||
[Display(Name = "Select Theme")]
|
||||
public string ThemeCssUri { get; set; }
|
||||
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
|
||||
[MaxLength(500)]
|
||||
[Display(Name = "Custom Theme CSS file")]
|
||||
public string CustomThemeCssUri { get; set; }
|
||||
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
|
||||
[MaxLength(500)]
|
||||
[Display(Name = "Custom bootstrap CSS file")]
|
||||
public string BootstrapCssUri { get; set; }
|
||||
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
|
||||
[Display(Name = "Custom Creative Start CSS file")]
|
||||
public string CreativeStartCssUri { get; set; }
|
||||
public bool FirstRun { get; set; }
|
||||
public override string ToString()
|
||||
|
@ -104,6 +104,7 @@ namespace BTCPayServer.Services.Wallets
|
||||
public void InvalidateCache(DerivationStrategyBase strategy)
|
||||
{
|
||||
_MemoryCache.Remove("CACHEDCOINS_" + strategy.ToString());
|
||||
_MemoryCache.Remove("CACHEDBALANCE_" + strategy.ToString());
|
||||
_FetchingUTXOs.TryRemove(strategy.ToString(), out var unused);
|
||||
}
|
||||
ConcurrentDictionary<string, TaskCompletionSource<UTXOChanges>> _FetchingUTXOs = new ConcurrentDictionary<string, TaskCompletionSource<UTXOChanges>>();
|
||||
@ -178,10 +179,14 @@ namespace BTCPayServer.Services.Wallets
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
public async Task<Money> GetBalance(DerivationStrategyBase derivationStrategy, CancellationToken cancellation = default(CancellationToken))
|
||||
public Task<decimal> GetBalance(DerivationStrategyBase derivationStrategy, CancellationToken cancellation = default(CancellationToken))
|
||||
{
|
||||
UTXOChanges changes = await GetUTXOChanges(derivationStrategy, cancellation);
|
||||
return changes.GetUnspentUTXOs().Select(c => c.Value).Sum();
|
||||
return _MemoryCache.GetOrCreateAsync("CACHEDBALANCE_" + derivationStrategy.ToString(), async (entry) =>
|
||||
{
|
||||
var result = await _Client.GetBalanceAsync(derivationStrategy, cancellation);
|
||||
entry.AbsoluteExpiration = DateTimeOffset.UtcNow + CacheSpan;
|
||||
return result.Total.GetValue(_Network);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,14 @@
|
||||
ViewData["Title"] = "Access denied";
|
||||
}
|
||||
|
||||
<header>
|
||||
<h2 class="text-danger">ViewData["Title"]</h2>
|
||||
<p class="text-danger">You do not have access to this resource.</p>
|
||||
</header>
|
||||
<section>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<h4>@ViewData["Title"]</h4>
|
||||
<hr />
|
||||
<p class="text-danger">You do not have access to this resource.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
@ -70,5 +70,6 @@
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer"><span>Already have an account? <a id="Login" asp-action="Login" asp-route-returnurl="@ViewData["ReturnUrl"]">Login</a></span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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()
|
||||
|
@ -100,7 +100,8 @@
|
||||
<span>{{ srvModel.btcDue }} {{ srvModel.cryptoCode }}</span>
|
||||
</div>
|
||||
<div class="single-item-order__right__ex-rate" v-if="srvModel.orderAmountFiat && srvModel.cryptoCode">
|
||||
{{srvModel.rateBaseAmount}} {{ srvModel.cryptoCodeSrv }} = {{ srvModel.rate }}
|
||||
<span v-if="srvModel.cryptoCodeSrv === 'Sats'">1 Sat = {{ srvModel.rate }}</span>
|
||||
<span v-else>1 {{ srvModel.cryptoCodeSrv }} = {{ srvModel.rate }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="fa fa-angle-double-down"></span>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -13,14 +13,23 @@
|
||||
<div class="row">
|
||||
<div class="col-lg-6">
|
||||
<form method="post">
|
||||
<div class="form-group mb-5">
|
||||
<h4>Custom theme</h4>
|
||||
<p>Select one of our predefined themes. Optionally you can also provide a CSS file that customizes the chosen theme.</p>
|
||||
<div class="form-group">
|
||||
<label asp-for="ThemeCssUri"></label>
|
||||
<select id=themes asp-for="ThemeCssUri" class="form-control">
|
||||
<select asp-for="ThemeCssUri" class="form-control">
|
||||
<option value="/main/themes/classic.css">Classic</option>
|
||||
<option value="/main/themes/casa.css">Casa</option>
|
||||
</select>
|
||||
<span asp-validation-for="ThemeCssUri" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group mb-5">
|
||||
<label asp-for="CustomThemeCssUri"></label>
|
||||
<a href="https://docs.btcpayserver.org/development/theme#custom-themes" target="_blank"><span class="fa fa-question-circle-o" title="More information..."></span></a>
|
||||
<input asp-for="CustomThemeCssUri" class="form-control" />
|
||||
<span asp-validation-for="CustomThemeCssUri" class="text-danger"></span>
|
||||
</div>
|
||||
<h4>Bootstrap theme</h4>
|
||||
<div class="form-group">
|
||||
<label asp-for="BootstrapCssUri"></label>
|
||||
<a href="https://docs.btcpayserver.org/development/theme#bootstrap-themes" target="_blank"><span class="fa fa-question-circle-o" title="More information..."></span></a>
|
||||
|
@ -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>
|
||||
|
@ -7,13 +7,17 @@
|
||||
<meta name="author" content="">
|
||||
@if (themeManager.DiscourageSearchEngines)
|
||||
{
|
||||
<META NAME="robots" CONTENT="noindex">
|
||||
<meta name="robots" content="noindex">
|
||||
}
|
||||
<title>@ViewData["Title"]</title>
|
||||
@* CSS *@
|
||||
<link href="@this.Context.Request.GetRelativePathOrAbsolute(themeManager.BootstrapUri)" rel="stylesheet" />
|
||||
<link href="@this.Context.Request.GetRelativePathOrAbsolute(themeManager.CreativeStartUri)" rel="stylesheet" />
|
||||
<link href="@this.Context.Request.GetRelativePathOrAbsolute(themeManager.ThemeUri)" rel="stylesheet" />
|
||||
@if (!String.IsNullOrWhiteSpace(themeManager.CustomThemeUri))
|
||||
{
|
||||
<link href="@this.Context.Request.GetRelativePathOrAbsolute(themeManager.CustomThemeUri)" rel="stylesheet" />
|
||||
}
|
||||
<bundle name="wwwroot/bundles/main-bundle.min.css" />
|
||||
@* JS *@
|
||||
<bundle name="wwwroot/bundles/main-bundle.min.js" />
|
||||
|
56
BTCPayServer/Views/Shared/VaultElements.cshtml
Normal file
56
BTCPayServer/Views/Shared/VaultElements.cshtml
Normal 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" type="password" class="form-control" placeholder="Passphrase" />
|
||||
</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" type="password" class="form-control" placeholder="Passphrase confirmation" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
@ -6,6 +6,7 @@
|
||||
|
||||
@section HeadScripts {
|
||||
<style type="text/css">
|
||||
|
||||
.hw-fields {
|
||||
display: none;
|
||||
}
|
||||
@ -18,12 +19,42 @@
|
||||
<div asp-validation-summary="All" class="text-danger"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal fade" id="btcpayservervault" tabindex="-1" role="dialog" aria-labelledby="btcpayservervault" aria-hidden="true">
|
||||
|
||||
</div>
|
||||
<partial name="VaultElements" />
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div id="WebsocketPath" style="display:none;">@Url.Action("VaultBridgeConnection", "Vault", new { cryptoCode = Model.CryptoCode })</div>
|
||||
@if (!Model.Confirmation)
|
||||
{
|
||||
<partial name="AddDerivationSchemes_HardwareWalletDialogs" model="@Model" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<template id="btcpayservervault_template">
|
||||
<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">Address verification</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Confirm on the device that you see address <b id="displayedAddress"></b></p>
|
||||
<div class="form-group">
|
||||
<div id="vaultPlaceholder"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
|
||||
<button id="vault-confirm" class="btn btn-primary" style="display:none;"></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
}
|
||||
<form method="post">
|
||||
|
||||
<input id="Config" asp-for="Config" type="hidden" />
|
||||
@ -44,12 +75,16 @@
|
||||
|
||||
<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
|
||||
<button class="btn btn-link dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
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">
|
||||
<button class="dropdown-item" type="button">... Coldcard (air gap)</button>
|
||||
<button class="dropdown-item check-for-ledger" type="button">... Ledger Wallet</button>
|
||||
@if (Model.CryptoCode == "BTC")
|
||||
{
|
||||
<button class="dropdown-item check-for-vault" type="button">... the vault (preview)</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -130,6 +165,10 @@
|
||||
<tr>
|
||||
<th>Key path</th>
|
||||
<th>Address</th>
|
||||
@if (Model.Source == "Vault")
|
||||
{
|
||||
<th>Actions</th>
|
||||
}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@ -138,6 +177,10 @@
|
||||
<tr>
|
||||
<td>@sample.KeyPath</td>
|
||||
<td>@sample.Address</td>
|
||||
@if (Model.Source == "Vault")
|
||||
{
|
||||
<td><a class="showaddress" href="#" onclick='showAddress(@Safe.Json(sample.RootedKeyPath.ToString()), @Safe.Json(sample.Address)); return false;'>Show on device</a></td>
|
||||
}
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
@ -162,6 +205,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>
|
||||
|
@ -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,45 @@
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template id="btcpayservervault_template">
|
||||
<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">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>You may import from BTCPayServer Vault.</p>
|
||||
<div class="form-group">
|
||||
<div id="vaultPlaceholder"></div>
|
||||
</div>
|
||||
<div id="vault-xpub" style="display:none;">
|
||||
<div class="form-group">
|
||||
<label for="addressType">Address type</label>
|
||||
<select name="addressType" class="form-control">
|
||||
<option value="segwit">Segwit (Recommended, cheapest transaction fee)</option>
|
||||
<option value="segwitWrapped">Segwit wrapped (less cheap but compatible with old wallets)</option>
|
||||
<option value="legacy">Legacy (Not recommended)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="accountNumber">Account</label>
|
||||
<select name="accountNumber" class="form-control">
|
||||
@for (int i = 0; i < 20; i++)
|
||||
{
|
||||
<option value="@i">@i</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
|
||||
<button id="vault-confirm" class="btn btn-primary" style="display:none;"></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -66,7 +66,7 @@
|
||||
</tbody>
|
||||
</table>
|
||||
<p>Note that the <code>certthumbprint</code> to connect to your LND node can be obtained through this command line:</p>
|
||||
<p><pre><code>openssl x509 -noout -fingerprint -sha256 -inform pem -in /root/.lnd/tls.cert</code></pre></p>
|
||||
<p><pre><code>openssl x509 -noout -fingerprint -sha256 -inform pem -in /root/.lnd/tls.cert | sed -e 's/.*=//' -e 's/://g'</code></pre></p>
|
||||
<p>You can omit <code>certthumbprint</code> if you the certificate is trusted by your machine</p>
|
||||
<p>You can set <code>allowinsecure</code> to <code>true</code> if your LND REST server is using HTTP or HTTPS with an untrusted certificate which you don't know the <code>certthumbprint</code></p>
|
||||
</div>
|
||||
|
@ -18,7 +18,7 @@
|
||||
<div class="col-lg-12 section-heading">
|
||||
<h2>@ViewData["Title"]</h2>
|
||||
<hr class="primary">
|
||||
<p>Create and manage wallets. <a href="https://docs.btcpayserver.org/features/wallet" target="_blank"><span class="fa fa-question-circle-o" title="More information..."></span></a></p>
|
||||
<p>Connect and manage wallets. <a href="https://docs.btcpayserver.org/features/wallet" target="_blank"><span class="fa fa-question-circle-o" title="More information..."></span></a></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -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">
|
||||
|
@ -12,7 +12,7 @@
|
||||
</div>
|
||||
}
|
||||
<div class="row">
|
||||
<div class="col-lg-12 section-heading">
|
||||
<div class="col-lg-12 section-heading text-center">
|
||||
<h2>Transaction review</h2>
|
||||
<hr class="primary">
|
||||
@if (Model.CanCalculateBalance)
|
||||
|
@ -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>
|
||||
|
58
BTCPayServer/Views/Wallets/WalletSendVault.cshtml
Normal file
58
BTCPayServer/Views/Wallets/WalletSendVault.cshtml
Normal file
@ -0,0 +1,58 @@
|
||||
@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">×</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>
|
||||
<button id="vault-confirm" class="btn btn-primary" style="display:none;"></button>
|
||||
</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">
|
||||
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);
|
||||
if (await vaultUI.askForDevice() && await vaultUI.askSignPSBT({
|
||||
walletId: $("#WalletId").val(),
|
||||
psbt: $("#PSBT").val()
|
||||
})) {
|
||||
$("#PSBT").val(vaultUI.psbt);
|
||||
$("#broadcastForm").submit();
|
||||
}
|
||||
}
|
||||
$(function () {
|
||||
askSign();
|
||||
});
|
||||
</script>
|
||||
}
|
@ -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>
|
||||
|
@ -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>
|
||||
|
120
BTCPayServer/WebSocketHelper.cs
Normal file
120
BTCPayServer/WebSocketHelper.cs
Normal 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 { } }
|
||||
}
|
||||
}
|
||||
}
|
69
BTCPayServer/wwwroot/img/lunanode.svg
Normal file
69
BTCPayServer/wwwroot/img/lunanode.svg
Normal 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 |
2
BTCPayServer/wwwroot/img/walletofsatoshi.svg
Normal file
2
BTCPayServer/wwwroot/img/walletofsatoshi.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 5.3 KiB |
@ -1,5 +1,4 @@
|
||||
function initLedger(){
|
||||
|
||||
function initLedger() {
|
||||
var ledgerDetected = false;
|
||||
|
||||
var loc = window.location, new_uri;
|
||||
@ -88,14 +87,76 @@
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
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 () {
|
||||
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");
|
||||
}
|
||||
|
||||
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();
|
||||
});
|
||||
if (await vaultUI.askForDevice() && await vaultUI.askForXPubs()) {
|
||||
displayXPubs(vaultUI.xpub);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
106
BTCPayServer/wwwroot/js/vaultbridge.js
Normal file
106
BTCPayServer/wwwroot/js/vaultbridge.js
Normal file
@ -0,0 +1,106 @@
|
||||
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) { };
|
||||
this.close = function () { if (websocket) websocket.close(); };
|
||||
/**
|
||||
* @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.overrideMimeType("text/plain");
|
||||
request.open('POST', 'http://127.0.0.1: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.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
|
||||
};
|
||||
})();
|
378
BTCPayServer/wwwroot/js/vaultbridge.ui.js
Normal file
378
BTCPayServer/wwwroot/js/vaultbridge.ui.js
Normal file
@ -0,0 +1,378 @@
|
||||
/// <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"),
|
||||
needInitialized: new VaultFeedback("failed", "The device has not been initialized.", "vault-feedback2", "need-initialized"),
|
||||
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"),
|
||||
askXpubs: new VaultFeedback("?", "Select your address type and account", "vault-feedback3", "fetching-xpubs"),
|
||||
fetchedXpubs: new VaultFeedback("ok", "Public keys successfully fetched.", "vault-feedback3", "xpubs-fetched"),
|
||||
unexpectedError: new VaultFeedback("failed", "An unexpected error happened. ({{0}})", "vault-feedback3", "unknown-error"),
|
||||
invalidNetwork: new VaultFeedback("failed", "The device is targetting a different chain.", "vault-feedback3", "invalid-network"),
|
||||
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. (Wrong device, wrong passphrase or wrong device fingerprint in your wallet settings)", "vault-feedback3", "wrong-wallet"),
|
||||
wrongKeyPath: new VaultFeedback("failed", "This device can't sign the transaction. (The wallet keypath in your wallet settings seems incorrect)", "vault-feedback3", "wrong-keypath"),
|
||||
needPassphrase: new VaultFeedback("?", "Enter the passphrase.", "vault-feedback3", "need-passphrase"),
|
||||
needPassphraseOnDevice: new VaultFeedback("?", "Please, enter the passphrase on the device.", "vault-feedback3", "need-passphrase-on-device"),
|
||||
signingTransaction: new VaultFeedback("?", "Please review and confirm the transaction on your device...", "vault-feedback3", "ask-signing"),
|
||||
reviewAddress: new VaultFeedback("?", "Please review the address on your device...", "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.xpub = 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) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
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.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.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;
|
||||
}
|
||||
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;
|
||||
};
|
||||
|
||||
/**
|
||||
* @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();
|
||||
resolve({
|
||||
addressType: $("select[name=\"addressType\"]").val(),
|
||||
accountNumber: parseInt($("select[name=\"accountNumber\"]").val())
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @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
|
||||
};
|
||||
})();
|
@ -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 3° 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.",
|
||||
|
@ -48,5 +48,5 @@
|
||||
"Pay with Changelly": "Pagar com Changelly",
|
||||
"Close": "Fechar",
|
||||
"NotPaid_ExtraTransaction": "A fatura não foi paga integralmente. Por favor, envie outra transação para cobrir o valor devido.",
|
||||
"Recommended_Fee": "Recommended fee: {{feeRate}} sat/byte"
|
||||
"Recommended_Fee": "Taxa recomendada: {{feeRate}} sat/byte"
|
||||
}
|
@ -48,5 +48,5 @@
|
||||
"Pay with Changelly": "Оплатить с помощью Changelly",
|
||||
"Close": "Закрыть",
|
||||
"NotPaid_ExtraTransaction": "Счет не был оплачен полностью. Пожалуйста, отправьте еще одну транзакцию для покрытия суммы задолженности.",
|
||||
"Recommended_Fee": "Recommended fee: {{feeRate}} sat/byte"
|
||||
"Recommended_Fee": "Рекомендуемая комиссия: {{feeRate}} сат/байт"
|
||||
}
|
@ -42,11 +42,6 @@ a {
|
||||
transition-property: background, color;
|
||||
}
|
||||
|
||||
/* Admin Sections */
|
||||
.section-heading {
|
||||
text-align: var(--btcpay-section-heading-text-align, center);
|
||||
}
|
||||
|
||||
/* Admin Sidebar Navigation */
|
||||
a.nav-link {
|
||||
color: var(--btcpay-nav-color-link, var(--btcpay-color-neutral-600));
|
||||
@ -122,3 +117,28 @@ 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;
|
||||
}
|
||||
.pin-button {
|
||||
height: 135px;
|
||||
margin-top: 20px;
|
||||
background-color: white;
|
||||
border: solid lightgray 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.pin-button:hover {
|
||||
background-color: lightgray;
|
||||
}
|
||||
|
@ -6,6 +6,7 @@
|
||||
--btcpay-brand-darker: #19154B;
|
||||
--btcpay-brand-darkest: #02000C;
|
||||
|
||||
/* Color definitions for specific purposes - map the general colors or define additional ones */
|
||||
--btcpay-color-white: #fff;
|
||||
--btcpay-color-black: #000;
|
||||
|
||||
@ -19,7 +20,6 @@
|
||||
--btcpay-color-neutral-800: #181334;
|
||||
--btcpay-color-neutral-900: #100d20;
|
||||
|
||||
/* Color definitions for specific purposes - map the general colors or define additional ones */
|
||||
--btcpay-color-primary: var(--btcpay-brand-medium); /* Usage: Text color */
|
||||
--btcpay-color-primary-backdrop: #baa4fd; /* Usage: Backgrounds, borders, shadows */
|
||||
--btcpay-color-primary-accent: #4b3bc0; /* Usage: Background on Focus/Hover */
|
||||
@ -55,13 +55,13 @@
|
||||
--btcpay-body-color-link: var(--btcpay-color-primary);
|
||||
--btcpay-body-color-link-accent: var(--btcpay-color-primary-accent);
|
||||
|
||||
--btcpay-section-heading-text-align: left;
|
||||
|
||||
--btcpay-nav-color-link-accent: var(--btcpay-color-neutral-100);
|
||||
|
||||
--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";
|
||||
|
@ -6,6 +6,7 @@
|
||||
--btcpay-brand-darker: #0F3B21;
|
||||
--btcpay-brand-darkest: #05120a;
|
||||
|
||||
/* Color definitions for specific purposes - map the general colors or define additional ones */
|
||||
--btcpay-color-white: #fff;
|
||||
--btcpay-color-black: #000;
|
||||
|
||||
@ -19,10 +20,9 @@
|
||||
--btcpay-color-neutral-800: #343a40;
|
||||
--btcpay-color-neutral-900: #212529;
|
||||
|
||||
/* Color definitions for specific purposes - map the general colors or define additional ones */
|
||||
--btcpay-color-primary: #329f80;
|
||||
--btcpay-color-primary-backdrop: rgba(81, 173, 147, 0.25);
|
||||
--btcpay-color-primary-accent: #267861;
|
||||
--btcpay-color-primary: #329f80; /* Usage: Text color */
|
||||
--btcpay-color-primary-backdrop: rgba(81, 173, 147, 0.25); /* Usage: Backgrounds, borders, shadows */
|
||||
--btcpay-color-primary-accent: #267861; /* Usage: Background on Focus/Hover */
|
||||
--btcpay-color-secondary: var(--btcpay-color-neutral-600);
|
||||
--btcpay-color-secondary-backdrop: rgba(130, 138, 145, 0.25);
|
||||
--btcpay-color-secondary-accent: var(--btcpay-color-neutral-800);
|
||||
@ -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;
|
||||
|
@ -1,76 +0,0 @@
|
||||
:root {
|
||||
--btcpay-color-white: #fff;
|
||||
--btcpay-color-black: #000;
|
||||
|
||||
--btcpay-color-neutral-100: #FBFAF8;
|
||||
--btcpay-color-neutral-200: #F5F5F3;
|
||||
--btcpay-color-neutral-300: #E5E5E5;
|
||||
--btcpay-color-neutral-400: #ced4da;
|
||||
--btcpay-color-neutral-500: #adb5bd;
|
||||
--btcpay-color-neutral-600: #536E8D;
|
||||
--btcpay-color-neutral-700: #465C76;
|
||||
--btcpay-color-neutral-800: #244B71;
|
||||
--btcpay-color-neutral-900: #202C39;
|
||||
|
||||
--btcpay-color-primary: #329f80;
|
||||
--btcpay-color-primary-accent: #267861;
|
||||
--btcpay-color-primary-backdrop: rgba(81, 173, 147, 0.2);
|
||||
--btcpay-color-secondary: var(--btcpay-color-neutral-600);
|
||||
--btcpay-color-secondary-accent: var(--btcpay-color-neutral-800);
|
||||
--btcpay-color-secondary-backdrop: rgba(130, 138, 145, 0.25);
|
||||
--btcpay-color-success: #329f80;
|
||||
--btcpay-color-success-accent: #1e7e34;
|
||||
--btcpay-color-success-backdrop: rgba(72, 180, 97, 0.25);
|
||||
--btcpay-color-info: #17a2b8;
|
||||
--btcpay-color-info-accent: #117a8b;
|
||||
--btcpay-color-info-backdrop: rgba(58, 176, 195, 0.25);
|
||||
--btcpay-color-warning: #ffc107;
|
||||
--btcpay-color-warning-accent: #d39e00;
|
||||
--btcpay-color-warning-backdrop: rgba(222, 170, 12, 0.25);
|
||||
--btcpay-color-danger: #dc3545;
|
||||
--btcpay-color-danger-accent: #bd2130;
|
||||
--btcpay-color-danger-backdrop: rgba(225, 83, 97, 0.25);
|
||||
--btcpay-color-light: var(--btcpay-color-neutral-100);
|
||||
--btcpay-color-light-accent: #dae0e5;
|
||||
--btcpay-color-light-backdrop: rgba(216, 217, 219, 0.25);
|
||||
--btcpay-color-dark: var(--btcpay-color-neutral-900);
|
||||
--btcpay-color-dark-accent: #1d2124;
|
||||
--btcpay-color-dark-backdrop: rgba(82, 88, 93, 0.25);
|
||||
|
||||
--btcpay-bg-body: var(--btcpay-color-neutral-100);
|
||||
--btcpay-bg-dark: var(--btcpay-color-neutral-900);
|
||||
--btcpay-bg-tile: var(--btcpay-color-white);
|
||||
--btcpay-bg-cta: var(--btcpay-bg-dark);
|
||||
|
||||
--btcpay-body-color: var(--btcpay-color-neutral-900);
|
||||
--btcpay-body-color-link: var(--btcpay-color-primary);
|
||||
--btcpay-body-color-link-accent: var(--btcpay-color-primary-accent);
|
||||
|
||||
--btcpay-section-heading-text-align: left;
|
||||
|
||||
--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";
|
||||
--btcpay-font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
}
|
||||
|
||||
/* Bootstrap modifications */
|
||||
html {
|
||||
font-size: var(--btcpay-font-size-base);
|
||||
}
|
||||
|
||||
.bg-dark {
|
||||
background-color: var(--btcpay-bg-dark) !important;
|
||||
}
|
||||
|
||||
header.masthead::before,
|
||||
.service-box img {
|
||||
filter: hue-rotate(410deg) saturate(33%); }
|
||||
|
||||
.table {
|
||||
margin: 1.5rem 0 3rem;
|
||||
}
|
||||
|
||||
.table th {
|
||||
border-top: 0;
|
||||
}
|
578
BTCPayServer/wwwroot/vendor/popper/popper.js
vendored
578
BTCPayServer/wwwroot/vendor/popper/popper.js
vendored
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@ -1,5 +1,5 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<Version>1.0.3.138</Version>
|
||||
<Version>1.0.3.146</Version>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
51
Docs/Themes.md
Normal file
51
Docs/Themes.md
Normal file
@ -0,0 +1,51 @@
|
||||
# Developing and extending themes
|
||||
|
||||
The BTCPay Server user interface is built on a customized version of Bootstrap that supports [CSS custom properties](https://developer.mozilla.org/en-US/docs/Web/CSS/--*).
|
||||
This allows us to change theme related settings like fonts and colors without affecting the [`bootstrap.css`](#Notes-on-bootstrapcss).
|
||||
Also we can provide just the relevant customized parts instead of shipping a whole `bootstrap.css` file for each theme.
|
||||
|
||||
Take a look at the [predefined themes](../BTCPayServer/wwwroot/main/themes) to get an overview of this approach.
|
||||
|
||||
## Modifying existing themes
|
||||
|
||||
The custom property definitions in the `:root` selector are divided into several sections, that can be seen as a cascade:
|
||||
|
||||
- The first section contains general definitions (i.e. for custom brand and neutral colors).
|
||||
- The second section defines variables for specific purposes.
|
||||
Here you can map the general definitions or create additional ones.
|
||||
- The third section contains definitions for specific parts of the page, sections or components.
|
||||
Here you should try to reuse definitions from above as much as possible to provide a consistent look and feel.
|
||||
|
||||
The variables defined in a theme file get used in the [`site.css`](../BTCPayServer/wwwroot/main/site.css) and [`creative.css`](../BTCPayServer/wwwroot/main/bootstrap4-creativestart/creative.css) files.
|
||||
|
||||
### Overriding Bootstrap selectors
|
||||
|
||||
In addition to the variables you can also provide styles by directly adding CSS selectors to this file.
|
||||
This can be seen as a last resort in case there is no variable for something you want to change or some minor tweaking.
|
||||
|
||||
### Adding theme variables
|
||||
|
||||
In general it is a good idea to introduce specific variables for special purposes (like setting the link colors of a specific section).
|
||||
This allows us to address individual portions of the styles without affecting other parts which might be tight to a general variable.
|
||||
|
||||
For cases in which you want to introduce new variables that are used across all themes, add them to the `site.css` file.
|
||||
This file contains our modifications of the Bootstrap styles.
|
||||
Refrain from modifying `bootstrap.css` directly – see the [additional notes](#Notes-on-bootstrapcss) for the reasoning behind this.
|
||||
|
||||
## Adding a new theme
|
||||
|
||||
You should copy one of our predefined themes and change the variables to fit your needs.
|
||||
|
||||
To test and play around with the adjustments, you can also use the developer tools of the browser:
|
||||
Inspect the `<html>` element and modify the variables in the `:root` section of the styles inspector.
|
||||
|
||||
## Notes on bootstrap.css
|
||||
|
||||
The `bootstrap.css` file itself is generated based on what the original vendor `bootstrap.css` provides.
|
||||
|
||||
Right now [Bootstrap](https://getbootstrap.com/docs/4.3/getting-started/theming/) does not use custom properties, but in the future it is likely that they might switch to this approach as well.
|
||||
Until then we created a build script [in this repo](https://github.com/dennisreimann/btcpayserver-ui-prototype) which generates the `bootstrap.css` file we are using here.
|
||||
|
||||
The general approach should be to not modify the `bootstrap.css`, so that we can keep it easily updatable.
|
||||
The initial modifications of this file were made in order to allow for this themeing approach.
|
||||
Because bootstrap has colors spread all over the place we'd otherwise have to override mostly everything, that's why these general modifications are in the main `bootstrap.css` file.
|
24
README.md
24
README.md
@ -7,7 +7,7 @@
|
||||
|
||||
# BTCPay Server
|
||||
|
||||
## Introduction
|
||||
## Introduction
|
||||
|
||||
BTCPay Server is a free and open-source cryptocurrency payment processor which allows you to receive payments in Bitcoin and altcoins directly, with no fees, transaction cost or a middleman.
|
||||
|
||||
@ -58,13 +58,13 @@ Thanks to the [apps](https://github.com/btcpayserver/btcpayserver-doc/blob/maste
|
||||
|
||||
## Getting Started
|
||||
|
||||
Firstly, decide if you want to host an instance yourself or use a [third-party host](https://docs.btcpayserver.org/deployment/thirdpartyhosting). If you've chosen to self-host, we documented plenty [ways to deploy BTCPay Server](https://docs.btcpayserver.org/deployment/deployment).
|
||||
Firstly, decide if you want to host an instance yourself or use a [third-party host](https://docs.btcpayserver.org/deployment/thirdpartyhosting). If you've chosen to self-host, we documented plenty [ways to deploy BTCPay Server](https://docs.btcpayserver.org/deployment/deployment).
|
||||
|
||||
After successful deployment, make sure to check our [getting started](https://docs.btcpayserver.org/getting-started/registeraccount) and [walkthrough](https://docs.btcpayserver.org/btcpay-basics/walkthrough) guides. In case you would like to use Lightning Network, see [Lightning guide](https://docs.btcpayserver.org/features/lightningnetwork).
|
||||
|
||||
## Documentation
|
||||
|
||||
Please check out our [official website](https://btcpayserver.org/), our [complete documentation](https://github.com/btcpayserver/btcpayserver-doc) and [FAQ](https://github.com/btcpayserver/btcpayserver-doc/tree/master/FAQ#btcpay-frequently-asked-questions-and-common-issues) for more details.
|
||||
Please check out our [official website](https://btcpayserver.org/), our [complete documentation](https://github.com/btcpayserver/btcpayserver-doc) and [FAQ](https://github.com/btcpayserver/btcpayserver-doc/tree/master/FAQ#btcpay-frequently-asked-questions-and-common-issues) for more details.
|
||||
|
||||
If you have trouble using BTCPay, consider joining [communities listed on official website](https://btcpayserver.org/#communityCTA) to get help from BTCPay community members. Only file [Github issue](https://github.com/btcpayserver/btcpayserver/issues) for technical issues you can't resolve through other channels or feature requests you've validated with other members of community.
|
||||
|
||||
@ -82,6 +82,8 @@ Contributors looking to do something a bit more challenging, before opening a pu
|
||||
|
||||
You also have an awesome video of our contributors which explains how to get started.[](https://www.youtube.com/embed/VNMnd-dX9Q8)
|
||||
|
||||
Here is some info about [how to extend the themes](Docs/Themes.md)
|
||||
|
||||
## How to build
|
||||
|
||||
While the documentation advises to use docker-compose, you may want to build BTCPay yourself.
|
||||
@ -165,8 +167,22 @@ 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>
|
||||
|
||||
If you'd like to support the project, please visit the [donation page](https://btcpayserver.org/donate/).
|
||||
If you'd like to support the project, please visit the [donation page](https://btcpayserver.org/donate/).
|
||||
|
@ -1,4 +1,4 @@
|
||||
FROM mcr.microsoft.com/dotnet/core/sdk:2.1.505-alpine3.7 AS builder
|
||||
FROM mcr.microsoft.com/dotnet/core/sdk:2.1.607 AS builder
|
||||
WORKDIR /source
|
||||
COPY nuget.config nuget.config
|
||||
COPY Build/Common.csproj Build/Common.csproj
|
||||
@ -14,7 +14,7 @@ COPY BTCPayServer/. BTCPayServer/.
|
||||
COPY Build/Version.csproj Build/Version.csproj
|
||||
RUN cd BTCPayServer && dotnet publish --output /app/ --configuration Release
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/core/aspnet:2.1.9-alpine3.7
|
||||
FROM mcr.microsoft.com/dotnet/core/aspnet:2.1.14-alpine3.9
|
||||
|
||||
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT false
|
||||
RUN apk add --no-cache icu-libs openssh-keygen
|
||||
|
@ -1,5 +1,5 @@
|
||||
# This is a manifest image, will pull the image with the same arch as the builder machine
|
||||
FROM mcr.microsoft.com/dotnet/core/sdk:2.1.505 AS builder
|
||||
FROM mcr.microsoft.com/dotnet/core/sdk:2.1.607 AS builder
|
||||
RUN apt-get update \
|
||||
&& apt-get install -qq --no-install-recommends qemu qemu-user-static qemu-user binfmt-support
|
||||
|
||||
@ -19,7 +19,7 @@ COPY Build/Version.csproj Build/Version.csproj
|
||||
RUN cd BTCPayServer && dotnet publish --output /app/ --configuration Release
|
||||
|
||||
# Force the builder machine to take make an arm runtime image. This is fine as long as the builder does not run any program
|
||||
FROM mcr.microsoft.com/dotnet/core/aspnet:2.1.9-stretch-slim-arm32v7
|
||||
FROM mcr.microsoft.com/dotnet/core/aspnet:2.1.14-stretch-slim-arm32v7
|
||||
COPY --from=builder /usr/bin/qemu-arm-static /usr/bin/qemu-arm-static
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends iproute2 openssh-client \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
Reference in New Issue
Block a user