Compare commits

...

99 Commits

Author SHA1 Message Date
33703b83a3 Fix device not found with Trezor T 2019-12-10 23:52:48 +09:00
23b9dfed2c Cache balance 2019-12-10 22:17:59 +09:00
b8f6ef8844 fix sdk version of tests 2019-12-10 21:46:06 +09:00
6f6b4c8ead Fix dockerfile versions 2019-12-10 21:43:21 +09:00
5d87dd5861 Merge pull request #1219 from bolatovumar/fix-1217
[Wallet] Show invalid address message when address is invalid
2019-12-10 21:39:43 +09:00
02c8bf4469 bump 2019-12-10 21:23:38 +09:00
540cb912f3 Can display address on device at confirmation screen 2019-12-10 21:22:46 +09:00
93f490f570 Fix pin entry 2019-12-10 20:10:13 +09:00
de2e222ae5 Improve signing vault UI 2019-12-10 18:58:12 +09:00
12055a000b Proper handling of passphrase for Trezor T 2019-12-10 18:16:52 +09:00
6addb3e481 [Wallet] Show invalid address message when address is invalid
fix #1217
2019-12-07 21:20:42 -08:00
d9cd916440 Merge pull request #1187 from dennisreimann/document-themeing
Document themeing
2019-12-07 17:22:47 +09:00
452a705b75 Update russian,portugal 2019-12-07 17:21:11 +09:00
1c8206c749 Merge pull request #1214 from MrPaz/feature/satsfix
Fixing sats exchange rate display
2019-12-07 17:18:54 +09:00
062c42e524 Fix build 2019-12-07 16:42:03 +09:00
2d932ebb21 Update to runtime .NET 2.1.14 2019-12-07 16:35:13 +09:00
0e0fa53517 Prompt passphrase for trezor T 2019-12-05 22:47:07 +09:00
4e20730379 Better message when the device is signing (#1207) 2019-12-05 22:37:04 +09:00
1d70d935b8 Provide better details for unknown error (#1208) 2019-12-05 22:03:35 +09:00
9b8f42cdf6 Merge pull request #1213 from rockstardev/feature/accessdeniedfix
Fixing AccessDenied page displayed
2019-12-04 15:38:40 -06:00
eb85b1a7b4 Fixing sats exchange rate display
Fixed #1147
2019-12-04 14:21:33 -06:00
df7e2073df Fixing AccessDenied page displayed 2019-12-04 13:45:09 -06:00
84d943d6cc Trying to fix TrezorT 2019-12-04 22:12:38 +09:00
a1d82b0e8b fix bug if selecting segwit with vault 2019-12-04 21:52:48 +09:00
ab7c124302 Properly show text to enter pin or passphrase for trezor T 2019-12-04 17:23:10 +09:00
544597348b Fix inverted boolean logic 2019-12-04 17:19:25 +09:00
1559c510be Trying to handle Trezor T correctly 2019-12-04 17:16:37 +09:00
3e08581e9b Fix fetching xpub for trezor 2019-12-04 16:34:25 +09:00
49c70d9b04 Vault: Reinite the popup in add derivation scheme if it is closed 2019-12-04 16:16:41 +09:00
50e7d8389c Vault: Allow user to pick the account number 2019-12-04 15:54:08 +09:00
ea5bd6d435 bump nbx 2019-12-04 13:51:26 +09:00
cbb37674e3 vault-confirm is not a popper 2019-12-04 12:56:39 +09:00
e70d5ceb08 Fix JS error on WalletSendVault after PIN is asked (#1210) 2019-12-04 12:52:54 +09:00
5d7b253edd Fix bug: Vault option now showing up in PSBT 2019-12-03 18:57:07 +09:00
df38b84bbb Fix alignment in psbt review 2019-12-03 14:30:29 +09:00
a3fc75ea3b Show proper error message if the keypath in wallet settings is not good (Fix #1209) 2019-12-03 14:26:52 +09:00
71a8166027 Handle "pin already prompted" error. (Fix #1209) 2019-12-03 13:53:58 +09:00
564dd95d81 Remove unused modern theme
We can start over with this once the general design decisions are decided on and we start implementing a new UI.
2019-12-02 11:19:17 +01:00
eb16b435df Document themeing approach
Sums up and adds to the comments made in #1175.

I wasn't sure where to put these kinds of docs, as tehy are code related and not end user facing. Let's discuss whether or not this fits in here or should become part of the docs repo.

fix
2019-12-02 11:18:33 +01:00
f94daed06d Merge pull request #1184 from bolatovumar/fix-1182
[Wallet] Specify validation range for wallet send amount
2019-12-02 18:29:45 +09:00
e841bfa148 Merge pull request #1186 from dennisreimann/custom-theme
UI: Add custom theme file option
2019-12-02 18:29:07 +09:00
03b76380e7 Merge pull request #1192 from dennisreimann/ln-node-certthumbprint
LN Node: Show ready to use certthumbprint
2019-12-02 18:28:11 +09:00
c6671f7122 Merge pull request #1200 from pavlenex/ipn-notification
Re-name the Invoice e-mail notification field
2019-12-02 18:27:52 +09:00
db56c2b106 Merge pull request #1202 from pavlenex/create-wallet
Change create with connect in wallet page
2019-12-02 18:27:40 +09:00
f7b2c836b7 Merge pull request #1203 from Kukks/nbxupdate
update nbx + prep bitcoin payment data ctor change
2019-12-02 18:27:28 +09:00
6ec379b538 Merge pull request #1198 from Eskyee/patch-3
Update vaultbridge.js
2019-12-02 18:27:02 +09:00
d6e1d34ebf update nbx docker tests 2019-12-02 10:10:24 +01:00
bc2a039ea2 bump nbx and make balance return decimal 2019-12-02 09:57:38 +01:00
e31e600144 update nbx + prep bitcoin payment data ctor change 2019-12-01 15:30:56 +01:00
91f83d751f Connect wallet clarification 2019-11-30 18:15:28 +01:00
b8288f1efa Use 127.0.0.1 instead of localhost for vault's http requests. (Fix https://github.com/btcpayserver/BTCPayServer.Vault/issues/9#issuecomment-559858955) 2019-11-30 23:34:47 +09:00
fa61c2bcdd Add error for invalid network 2019-11-30 23:33:42 +09:00
2d168e1d9b Re-name the Invoice e-mail notification field 2019-11-30 11:45:06 +01:00
3e7ad4a4f6 Update vaultbridge.js
replace localhost with 127.0.0.1 fixes BTCPay Vault in Apple Safari browser Version 13.0.3 macOS Catalina 10.15.1 (19B88)
thanks to Nicolas
2019-11-29 20:09:46 +00:00
d2357ee8b9 Do not ask for Pin on Trezor T 2019-11-25 11:50:49 +09:00
9f04c7d0bc Merge pull request #1193 from dennisreimann/heading-alignment
UI: Left align admin section headings
2019-11-25 11:44:25 +09:00
9baa7eed80 UI: Left align admin section headings
Removes the custom property and consistently align the headings left, which I agree is the better choice.

Context: https://chat.btcpayserver.org/btcpayserver/pl/upb3nzch7j8nmpbt7kfmoe6beo
2019-11-24 20:39:29 +01:00
287fbc523f Asks pin and passphrase if device is not ready 2019-11-24 22:51:13 +09:00
ea6df35c87 LN Node: Show ready to use certthumbprint
Turns the output of the `openssl` command from

`SHA256 Fingerprint=48:37:A4:D6:[…]`

into this, which can be copied and inserted directly:

`4837A4D6[…]`
2019-11-24 09:48:34 +01:00
2e0db1a430 Fix XML parsing error (#1185) 2019-11-23 14:04:26 +09:00
7ccf0c3612 UI: Add custom theme file option
This adds a "Custom Theme CSS file" to the "Server settings: Theme" page. It works similar to the custom bootstrap and creative start fields, but is only added to the head in case it is set.

The main idea here is that users would choose one of our predefined themes and have the option to override specific values. The other apporach would be to let the custom theme replace the predefined one, but this could lead to issues in case we extend the set of variables, which would be unset then.
This way users can copy a predefined theme and override all variables or just the ones they want to customize.

The help icon besides the label links to a page in the docs I'm currently working on.
2019-11-22 21:09:18 +01:00
1b58058796 Refactoring 2019-11-22 19:19:05 +09:00
98276bcb3d Remove useless try/catch 2019-11-22 19:16:32 +09:00
1aa4dbb90d bump 2019-11-22 19:14:15 +09:00
eef623fb4b Ask passphrase when appropriate (Fix #1185) 2019-11-22 19:13:04 +09:00
84bbbcbe10 [Wallet] Specify validation range for wallet send amount
fix #1182
2019-11-21 18:47:10 -08:00
a324e2aeaf Merge pull request #1181 from Kukks/register-login-link
add login button to register page
2019-11-21 18:01:39 +09:00
d6b3530384 add login button to register page 2019-11-21 09:28:35 +01:00
dfe8a10e1a bump 2019-11-21 16:39:18 +09:00
5afa847e6e Fix sign transaction 2019-11-21 16:38:43 +09:00
1bcbc9bab0 Should not show the password 2019-11-21 16:03:06 +09:00
b915544798 Prevent NRE on NBxplorer listener 2019-11-21 14:27:57 +09:00
2b9f390c64 update translation 2019-11-21 14:17:27 +09:00
ee42d5c7b4 bump 2019-11-21 14:15:04 +09:00
f809dd51a6 Merge pull request #1152 from NicolasDorier/feature/vault
Add hardware support via BTCPayServer Vault
2019-11-21 14:13:43 +09:00
1a8d6e5c05 Implement BTCPayServer vault derivation scheme import 2019-11-21 14:06:16 +09:00
869ba745b2 Merge pull request #1175 from bolatovumar/fix-1169
Add CSS variable for preformatted text color
2019-11-21 14:03:05 +09:00
180dfb6edf Add CSS variable for preformatted text color
fix #1169
2019-11-20 11:12:31 -08:00
45b08ac8d2 Add sponsor 2019-11-19 20:37:15 +09:00
9a4b385432 Add sponsor 2019-11-19 20:33:26 +09:00
08289b89c5 Merge pull request #1176 from pavlenex/supporters-sw
Add new supporter to readme
2019-11-19 20:32:42 +09:00
a31d1d81c8 Update README.md 2019-11-19 09:20:31 +01:00
e4c7bb0378 add wallet of satoshi to readme
add wallet of Satoshi, fix Lunanode spacing
2019-11-19 09:16:04 +01:00
374aaf2e2b add walletofsatoshi svg logo 2019-11-19 09:12:38 +01:00
52fd686993 Merge pull request #1174 from pavlenex/new-supporters-lw
add new supporter to readme
2019-11-19 00:31:06 +09:00
03c36ef0d2 add lunanode to readme 2019-11-18 15:55:56 +01:00
71a6ffac2e Adjust status message for WalletTransactions 2019-11-18 21:47:09 +09:00
92777ba181 Make sure SSH does not throw in separate thread 2019-11-18 17:15:40 +09:00
81843fb609 Do not delete the password from the seed 2019-11-18 17:00:54 +09:00
3af3ffd038 Merge pull request #1167 from dennisreimann/ui-improvements
UI: Payment request improvement
2019-11-18 14:23:51 +09:00
2ce5cd0b6f Merge pull request #1171 from bitcoinbrisbane/master
Add office 365 to the quick fill settings
2019-11-18 14:23:12 +09:00
d791fd59e9 Merge branch 'master' of github.com:bitcoinbrisbane/btcpayserver 2019-11-18 08:12:30 +10:00
6064e3ce55 Add office365 quick settings 2019-11-18 08:12:06 +10:00
0fcfe0e977 Can prune wallet transaction history 2019-11-17 17:13:09 +09:00
997df5c64d Remove build warnings 2019-11-17 16:50:28 +09:00
27af96662f Fix bug for Network not having a NBitcoin Network 2019-11-17 13:04:42 +09:00
0be6f3ca70 UI: Remove superfluous spaces when description is empty 2019-11-16 23:28:53 +01:00
7af80611b6 UI: Better payment request amounts display
Uses a table instead of list group items, so that the columns properly align (rows use the same grid). Also aligns the values on the right.
2019-11-16 22:59:51 +01:00
929b5c7951 Add display attributes. Fix #98 2019-08-24 15:39:43 +10:00
69 changed files with 2218 additions and 479 deletions

View File

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

View File

@ -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>

View File

@ -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>

View File

@ -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'" />

View File

@ -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

View File

@ -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"

View File

@ -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>

View File

@ -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);

View File

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

View 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;
}
}
}

View File

@ -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

View File

@ -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));
}
}

View 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");
}
}
}
}

View File

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

View File

@ -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();
}
}
}
}

View File

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

View File

@ -22,7 +22,7 @@ namespace BTCPayServer.Models.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; }

View File

@ -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; }

View File

@ -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";
}
}

View File

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

View File

@ -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;
}
}
}

View File

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

View File

@ -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; }

View File

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

View File

@ -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;
}

View File

@ -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...");
}
}
}
}

View File

@ -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)

View File

@ -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()

View File

@ -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);
});
}
}
}

View File

@ -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>

View File

@ -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>

View File

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

View File

@ -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>

View File

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

View File

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

View File

@ -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>

View File

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

View File

@ -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" />

View File

@ -0,0 +1,56 @@
<script id="VaultConnection" type="text/template">
<div class="vault-feedback vault-feedback1">
<span class="vault-feedback-icon"></span> <span class="vault-feedback-content"></span>
</div>
<div class="vault-feedback vault-feedback2">
<span class="vault-feedback-icon"></span> <span class="vault-feedback-content"></span>
</div>
<div class="vault-feedback vault-feedback3">
<span class="vault-feedback-icon"></span> <span class="vault-feedback-content"></span>
</div>
<div id="pin-input" class="mt-4" style="display: none;">
<div class="row">
<div class="col">
<div class="input-group mb-2">
<input id="pin-display" type="text" class="form-control" readonly>
<div class="input-group-append">
<div id="pin-display-delete" class="input-group-text" style="cursor: pointer"><span class="fa fa-remove"></span></div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col"><div class="pin-button" id="pin-7"></div></div>
<div class="col"><div class="pin-button" id="pin-8"></div></div>
<div class="col"><div class="pin-button" id="pin-9"></div></div>
</div>
<div class="row">
<div class="col"><div class="pin-button" id="pin-4"></div></div>
<div class="col"><div class="pin-button" id="pin-5"></div></div>
<div class="col"><div class="pin-button" id="pin-6"></div></div>
</div>
<div class="row">
<div class="col"><div class="pin-button" id="pin-1"></div></div>
<div class="col"><div class="pin-button" id="pin-2"></div></div>
<div class="col"><div class="pin-button" id="pin-3"></div></div>
</div>
</div>
<div id="passphrase-input" class="mt-4" style="display: none;">
<div class="form-group">
<div class="input-group">
<div class="input-group-prepend">
<label for="Password" class="input-group-text"><span class="input-group-addon fa fa-lock"></span></label>
</div>
<input id="Password" 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>

View File

@ -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">&times;</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>

View File

@ -28,10 +28,9 @@
<p>
<a href="https://docs.btcpayserver.org/getting-started/connectwallet/ledgerwallet#manual-setup"
title="Open Ledger wallet manual setup docs"
target="_blank"
rel="noopener noreferrer"
>
title="Open Ledger wallet manual setup docs"
target="_blank"
rel="noopener noreferrer">
Can't find your account in the select?
</a>
</p>
@ -80,3 +79,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">&times;</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>

View File

@ -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>

View File

@ -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>

View File

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

View File

@ -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)

View File

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

View File

@ -0,0 +1,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">&times;</span></button>
<span id="alertMessage"></span>
</div>
<div class="row">
<div id="body" class="col-md-10">
<form id="broadcastForm" asp-action="WalletPSBTReady" method="post" style="display:none;">
<input type="hidden" id="WalletId" asp-for="WalletId" />
<input type="hidden" id="PSBT" asp-for="PSBT" />
<input type="hidden" asp-for="WebsocketPath" />
</form>
<div id="vaultPlaceholder"></div>
<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>
}

View File

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

View File

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

View File

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

View File

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

After

Width:  |  Height:  |  Size: 3.6 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@ -1,5 +1,4 @@
function initLedger(){
function initLedger() {
var ledgerDetected = false;
var loc = window.location, new_uri;
@ -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);
}
});
});

View 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
};
})();

View 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
};
})();

View File

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

View File

@ -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"
}

View File

@ -48,5 +48,5 @@
"Pay with Changelly": "Оплатить с помощью Changelly",
"Close": "Закрыть",
"NotPaid_ExtraTransaction": "Счет не был оплачен полностью. Пожалуйста, отправьте еще одну транзакцию для покрытия суммы задолженности.",
"Recommended_Fee": "Recommended fee: {{feeRate}} sat/byte"
"Recommended_Fee": "Рекомендуемая комиссия: {{feeRate}} сат/байт"
}

View File

@ -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;
}

View File

@ -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";

View File

@ -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;

View File

@ -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;
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -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
View 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.

View File

@ -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.[![Rockstar Dev and Britt Kelly - Btc Pay Server Code Along](https://img.youtube.com/vi/ZePbMPSIvHM/sddefault.jpg)](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/).

View File

@ -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

View File

@ -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/*