Compare commits

..

7 Commits

Author SHA1 Message Date
c1fe473cc7 remove from state when unreserved 2021-04-09 14:25:49 +02:00
d3923da8e4 pr changes 2021-04-08 13:41:29 +02:00
eac4d54466 add to docs 2021-04-08 13:41:29 +02:00
717836e9e0 style better 2021-04-08 13:41:29 +02:00
8d1ec4b5c0 show bip21 and additional work 2021-04-08 13:41:29 +02:00
1ee5c82b8b wip 2021-04-08 13:41:29 +02:00
c775c3cc78 Allow Payjoin for wallet receive addresses 2021-04-08 13:41:29 +02:00
321 changed files with 5260 additions and 5382 deletions
BTCPayServer.Client
BTCPayServer.Common
BTCPayServer.Data
BTCPayServer.Rating
BTCPayServer.Tests
BTCPayServer
BTCPayServer.csproj
Components/NotificationsDropdown
Configuration
Controllers
Data
DerivationSchemeSettings.csEventAggregator.cs
Events
Extensions.cs
Fido2
HostedServices
Hosting
ModelBinders
Models
PaymentRequest
Payments
Plugins
Program.cs
Security/GreenField
Services
Storage/ViewModels
U2F
Views
Account
Apps
AppsPublic
EthereumLikeStore
Fido2
Home
Invoice
Manage
MoneroLikeStore
Notifications
PaymentRequest
PullPayment
Server
Shared
Shopify
Stores
UserStores
ViewsRazor.cs
Wallets
wwwroot
Build
Changelog.mdREADME.md

@ -13,7 +13,7 @@
<RepositoryType>git</RepositoryType>
</PropertyGroup>
<PropertyGroup>
<Version Condition=" '$(Version)' == '' ">1.4.0</Version>
<Version Condition=" '$(Version)' == '' ">1.3.0</Version>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<PublishRepositoryUrl>true</PublishRepositoryUrl>
@ -27,7 +27,7 @@
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="NBitcoin" Version="5.0.77" />
<PackageReference Include="NBitcoin" Version="5.0.73" />
<PackageReference Include="BTCPayServer.Lightning.Common" Version="1.2.0" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
</ItemGroup>

@ -1,41 +1,21 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
using NBitcoin;
namespace BTCPayServer.Client
{
public partial class BTCPayServerClient
{
public virtual async Task<IEnumerable<InvoiceData>> GetInvoices(string storeId, string orderId = null, InvoiceStatus[] status = null,
DateTimeOffset? startDate = null,
DateTimeOffset? endDate = null,
bool includeArchived = false,
public virtual async Task<IEnumerable<InvoiceData>> GetInvoices(string storeId, bool includeArchived = false,
CancellationToken token = default)
{
Dictionary<string, object> queryPayload = new Dictionary<string, object>();
queryPayload.Add(nameof(includeArchived), includeArchived);
if (startDate is DateTimeOffset s)
queryPayload.Add(nameof(startDate), Utils.DateTimeToUnixTime(s));
if (endDate is DateTimeOffset e)
queryPayload.Add(nameof(endDate), Utils.DateTimeToUnixTime(e));
if (orderId != null)
queryPayload.Add(nameof(orderId), orderId);
if (status != null)
queryPayload.Add(nameof(status), status.Select(s=> s.ToString().ToLower()).ToArray());
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/invoices",
queryPayload), token);
new Dictionary<string, object>() {{nameof(includeArchived), includeArchived}}), token);
return await HandleResponse<IEnumerable<InvoiceData>>(response);
}

@ -5,7 +5,6 @@ using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;
@ -70,13 +69,6 @@ namespace BTCPayServer.Client
return JsonConvert.DeserializeObject<T>(str);
}
public async Task<T> SendHttpRequest<T>(string path,
Dictionary<string, object> queryPayload = null,
HttpMethod method = null, CancellationToken cancellationToken = default)
{
using var resp = await _httpClient.SendAsync(CreateHttpRequest(path, queryPayload, method), cancellationToken);
return await HandleResponse<T>(resp);
}
protected virtual HttpRequestMessage CreateHttpRequest(string path,
Dictionary<string, object> queryPayload = null,
HttpMethod method = null)

@ -21,6 +21,5 @@ namespace BTCPayServer.Client.Models
[JsonConverter(typeof(KeyPathJsonConverter))]
public KeyPath KeyPath { get; set; }
public string Address { get; set; }
public int Confirmations { get; set; }
}
}

@ -19,31 +19,7 @@ namespace BTCPayServer.Client.Models
}
public string DeliveryId { get; set; }
public string WebhookId { get; set; }
string _OriginalDeliveryId;
public string OriginalDeliveryId
{
get
{
if (_OriginalDeliveryId is null)
{
// Due to a typo in old version, we serialized `orignalDeliveryId` rather than `orignalDeliveryId`
// We silently fix that here.
// Note we can remove this code later on, as old webhook event are unlikely to be useful to anyone,
// and having a null orignalDeliveryId is not end of the world
if (AdditionalData != null &&
AdditionalData.TryGetValue("orignalDeliveryId", out var tok))
{
_OriginalDeliveryId = tok.Value<string>();
AdditionalData.Remove("orignalDeliveryId");
}
}
return _OriginalDeliveryId;
}
set
{
_OriginalDeliveryId = value;
}
}
public string OrignalDeliveryId { get; set; }
public bool IsRedelivery { get; set; }
[JsonConverter(typeof(StringEnumConverter))]
public WebhookEventType Type { get; set; }

@ -1,29 +0,0 @@
using NBitcoin;
using NBXplorer;
namespace BTCPayServer
{
public partial class BTCPayNetworkProvider
{
public void InitAlthash()
{
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("HTML");
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "Althash",
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://explorer.htmlcoin.com/api/tx/{0}" : "https://explorer.htmlcoin.com/api/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "htmlcoin",
DefaultRateRules = new[]
{
"HTML_X = HTML_USD",
"HTML_USD = hitbtc(HTML_USD)"
},
CryptoImagePath = "imlegacy/althash.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == ChainName.Mainnet ? new KeyPath("88'") : new KeyPath("1'")
});
}
}
}

@ -11,8 +11,8 @@ namespace BTCPayServer
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "BitCore",
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://explorer.bitcore.cc/tx/{0}" : "https://explorer.bitcore.cc/tx/{0}",
DisplayName = "Bitcore",
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://insight.bitcore.cc/tx/{0}" : "https://insight.bitcore.cc/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "bitcore",
DefaultRateRules = new[]

@ -51,7 +51,6 @@ namespace BTCPayServer
InitMonacoin();
InitDash();
InitFeathercoin();
InitAlthash();
InitGroestlcoin();
InitViacoin();
InitMonero();
@ -132,10 +131,5 @@ namespace BTCPayServer
}
return network as T;
}
public bool TryGetNetwork<T>(string cryptoCode, out T network) where T : BTCPayNetworkBase
{
network = GetNetwork<T>(cryptoCode);
return network != null;
}
}
}

@ -4,9 +4,9 @@
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="NBXplorer.Client" Version="3.0.21" />
<PackageReference Include="NBXplorer.Client" Version="3.0.20" />
</ItemGroup>
<ItemGroup Condition="'$(Altcoins)' != 'true'">
<Compile Remove="Altcoins\**\*.cs"></Compile>
</ItemGroup>
</Project>
</Project>

@ -55,7 +55,6 @@ namespace BTCPayServer.Data
public DbSet<StoreWebhookData> StoreWebhooks { get; set; }
public DbSet<StoreData> Stores { get; set; }
public DbSet<U2FDevice> U2FDevices { get; set; }
public DbSet<Fido2Credential> Fido2Credentials { get; set; }
public DbSet<UserStore> UserStore { get; set; }
public DbSet<WalletData> Wallets { get; set; }
public DbSet<WalletTransactionData> WalletTransactions { get; set; }
@ -100,7 +99,6 @@ namespace BTCPayServer.Data
StoreWebhookData.OnModelCreating(builder);
//StoreData.OnModelCreating(builder);
U2FDevice.OnModelCreating(builder);
Fido2Credential.OnModelCreating(builder);
Data.UserStore.OnModelCreating(builder);
//WalletData.OnModelCreating(builder);
WalletTransactionData.OnModelCreating(builder);

@ -9,7 +9,6 @@ namespace BTCPayServer.Data
{
public bool RequiresEmailConfirmation { get; set; }
public List<StoredFile> StoredFiles { get; set; }
[Obsolete("U2F support has been replace with FIDO2")]
public List<U2FDevice> U2FDevices { get; set; }
public List<APIKeyData> APIKeys { get; set; }
public DateTimeOffset? Created { get; set; }
@ -17,6 +16,5 @@ namespace BTCPayServer.Data
public List<NotificationData> Notifications { get; set; }
public List<UserStore> UserStores { get; set; }
public List<Fido2Credential> Fido2Credentials { get; set; }
}
}

@ -1,32 +0,0 @@
using System;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
namespace BTCPayServer.Data
{
public class Fido2Credential
{
public string Name { get; set; }
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public string Id { get; set; }
public string ApplicationUserId { get; set; }
public byte[] Blob { get; set; }
public CredentialType Type { get; set; }
public enum CredentialType
{
FIDO2
}
public static void OnModelCreating(ModelBuilder builder)
{
builder.Entity<Fido2Credential>()
.HasOne(o => o.ApplicationUser)
.WithMany(i => i.Fido2Credentials)
.HasForeignKey(i => i.ApplicationUserId).OnDelete(DeleteBehavior.Cascade);
}
public ApplicationUser ApplicationUser { get; set; }
}
}

@ -1,6 +1,5 @@
using System;
using System.ComponentModel.DataAnnotations;
using BTCPayServer.Client.Models;
using Microsoft.EntityFrameworkCore;
using NBitcoin;
@ -54,4 +53,13 @@ namespace BTCPayServer.Data
}
}
}
public enum PayoutState
{
AwaitingApproval,
AwaitingPayment,
InProgress,
Completed,
Cancelled
}
}

@ -3,13 +3,11 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using BTCPayServer.Client.Models;
using Microsoft.EntityFrameworkCore;
using NBitcoin;
namespace BTCPayServer.Data
{
public class PullPaymentData
{
[Key]
@ -88,6 +86,7 @@ namespace BTCPayServer.Data
}
}
public static class PayoutExtensions
{
public static IQueryable<PayoutData> GetPayoutInPeriod(this IQueryable<PayoutData> payouts, PullPaymentData pp)

@ -23,12 +23,11 @@ namespace BTCPayServer.Data
internal static void OnModelCreating(ModelBuilder builder)
{
#pragma warning disable CS0618 // Type or member is obsolete
builder.Entity<U2FDevice>()
.HasOne(o => o.ApplicationUser)
.WithMany(i => i.U2FDevices)
.HasForeignKey(i => i.ApplicationUserId).OnDelete(DeleteBehavior.Cascade);
#pragma warning restore CS0618 // Type or member is obsolete
}
}
}

@ -1,47 +0,0 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace BTCPayServer.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20210314092253_Fido2Credentials")]
public partial class Fido2Credentials : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Fido2Credentials",
columns: table => new
{
Id = table.Column<string>(nullable: false),
Name = table.Column<string>(nullable: true),
ApplicationUserId = table.Column<string>(nullable: true),
Blob = table.Column<byte[]>(nullable: true),
Type = table.Column<int>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Fido2Credentials", x => x.Id);
table.ForeignKey(
name: "FK_Fido2Credentials_AspNetUsers_ApplicationUserId",
column: x => x.ApplicationUserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_Fido2Credentials_ApplicationUserId",
table: "Fido2Credentials",
column: "ApplicationUserId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Fido2Credentials");
}
}
}

@ -170,31 +170,6 @@ namespace BTCPayServer.Migrations
b.ToTable("AspNetUsers");
});
modelBuilder.Entity("BTCPayServer.Data.Fido2Credential", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("ApplicationUserId")
.HasColumnType("TEXT");
b.Property<byte[]>("Blob")
.HasColumnType("BLOB");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("ApplicationUserId");
b.ToTable("Fido2Credentials");
});
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
{
b.Property<string>("InvoiceDataId")
@ -983,14 +958,6 @@ namespace BTCPayServer.Migrations
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.Fido2Credential", b =>
{
b.HasOne("BTCPayServer.Data.ApplicationUser", "ApplicationUser")
.WithMany("Fido2Credentials")
.HasForeignKey("ApplicationUserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")

@ -6,7 +6,7 @@
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.6.0" />
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.7" />
<PackageReference Include="NBitcoin" Version="5.0.77" />
<PackageReference Include="NBitcoin" Version="5.0.73" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="0.6.3" />
</ItemGroup>

@ -1266,13 +1266,6 @@
"symbol":null,
"crypto":true
},
{
"name":"Althash",
"code":"HTML",
"divisibility":8,
"symbol":null,
"crypto":true
},
{
"name":"CHC",
"code":"CHC",

@ -50,14 +50,13 @@ namespace BTCPayServer.Tests
tester.ActivateLightning();
await tester.StartAsync();
var user = tester.NewAccount();
var cryptoCode = "BTC";
await user.GrantAccessAsync(true);
user.RegisterDerivationScheme(cryptoCode);
user.GrantAccess(true);
user.RegisterDerivationScheme("BTC");
user.RegisterDerivationScheme("LTC");
user.RegisterLightningNode(cryptoCode, LightningConnectionType.CLightning);
var btcNetwork = tester.PayTester.Networks.GetNetwork<BTCPayNetwork>(cryptoCode);
var invoice = await user.BitPay.CreateInvoiceAsync(
new Invoice
user.RegisterLightningNode("BTC", LightningConnectionType.CLightning);
var btcNetwork = tester.PayTester.Networks.GetNetwork<BTCPayNetwork>("BTC");
var invoice = user.BitPay.CreateInvoice(
new Invoice()
{
Price = 1.5m,
Currency = "USD",
@ -70,43 +69,36 @@ namespace BTCPayServer.Tests
Assert.Equal(3, invoice.CryptoInfo.Length);
var controller = user.GetController<StoresController>();
var lightningVm = (LightningNodeViewModel)Assert.IsType<ViewResult>(controller.SetupLightningNode(user.StoreId, cryptoCode)).Model;
var lightningVm = (LightningNodeViewModel)Assert.IsType<ViewResult>(controller.AddLightningNode(user.StoreId, "BTC")).Model;
Assert.True(lightningVm.Enabled);
var response = await controller.SetLightningNodeEnabled(user.StoreId, cryptoCode, false);
Assert.IsType<RedirectToActionResult>(response);
// Get enabled state from overview action
StoreViewModel storeModel;
response = controller.UpdateStore();
storeModel = (StoreViewModel)Assert.IsType<ViewResult>(response).Model;
var lnNode = storeModel.LightningNodes.Find(node => node.CryptoCode == cryptoCode);
Assert.NotNull(lnNode);
Assert.False(lnNode.Enabled);
lightningVm.Enabled = false;
controller.AddLightningNode(user.StoreId, lightningVm, "save", "BTC").GetAwaiter().GetResult();
lightningVm = (LightningNodeViewModel)Assert.IsType<ViewResult>(controller.AddLightningNode(user.StoreId, "BTC")).Model;
Assert.False(lightningVm.Enabled);
WalletSetupViewModel setupVm;
var storeId = user.StoreId;
response = await controller.GenerateWallet(storeId, cryptoCode, WalletSetupMethod.GenerateOptions, new GenerateWalletRequest());
var cryptoCode = "BTC";
var response = await controller.GenerateWallet(storeId, cryptoCode, WalletSetupMethod.GenerateOptions, new GenerateWalletRequest());
Assert.IsType<ViewResult>(response);
// Get enabled state from overview action
response = controller.UpdateStore();
storeModel = (StoreViewModel)Assert.IsType<ViewResult>(response).Model;
var derivationScheme = storeModel.DerivationSchemes.Find(scheme => scheme.Crypto == cryptoCode);
Assert.NotNull(derivationScheme);
Assert.True(derivationScheme.Enabled);
// Get setup view model from modify action
response = await controller.ModifyWallet(new WalletSetupViewModel { StoreId = storeId, CryptoCode = cryptoCode });
setupVm = (WalletSetupViewModel)Assert.IsType<ViewResult>(response).Model;
Assert.True(setupVm.Enabled);
// Disable wallet
response = controller.SetWalletEnabled(storeId, cryptoCode, false).GetAwaiter().GetResult();
// Only Enabling/Disabling the payment method must redirect to store page
setupVm.Enabled = false;
response = controller.UpdateWallet(setupVm).GetAwaiter().GetResult();
Assert.IsType<RedirectToActionResult>(response);
response = controller.UpdateStore();
storeModel = (StoreViewModel)Assert.IsType<ViewResult>(response).Model;
derivationScheme = storeModel.DerivationSchemes.Find(scheme => scheme.Crypto == cryptoCode);
Assert.NotNull(derivationScheme);
Assert.False(derivationScheme.Enabled);
var oldScheme = derivationScheme.Value;
response = await controller.ModifyWallet(new WalletSetupViewModel { StoreId = storeId, CryptoCode = cryptoCode });
setupVm = (WalletSetupViewModel)Assert.IsType<ViewResult>(response).Model;
Assert.False(setupVm.Enabled);
invoice = await user.BitPay.CreateInvoiceAsync(
var oldScheme = setupVm.DerivationScheme;
invoice = user.BitPay.CreateInvoice(
new Invoice
{
Price = 1.5m,
@ -121,7 +113,7 @@ namespace BTCPayServer.Tests
Assert.Equal("LTC", invoice.CryptoInfo[0].CryptoCode);
// Removing the derivation scheme, should redirect to store page
response = controller.ConfirmDeleteWallet(user.StoreId, cryptoCode).GetAwaiter().GetResult();
response = controller.ConfirmDeleteWallet(user.StoreId, "BTC").GetAwaiter().GetResult();
Assert.IsType<RedirectToActionResult>(response);
// Setting it again should show the confirmation page
@ -180,8 +172,8 @@ namespace BTCPayServer.Tests
Assert.Equal(expected.ToJson(), onchainBTC.ToJson());
// Let's check that the root hdkey and account key path are taken into account when making a PSBT
invoice = await user.BitPay.CreateInvoiceAsync(
new Invoice
invoice = user.BitPay.CreateInvoice(
new Invoice()
{
Price = 1.5m,
Currency = "USD",
@ -192,7 +184,7 @@ namespace BTCPayServer.Tests
}, Facade.Merchant);
tester.ExplorerNode.Generate(1);
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo.First(c => c.CryptoCode == cryptoCode).Address,
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo.First(c => c.CryptoCode == "BTC").Address,
tester.ExplorerNode.Network);
tester.ExplorerNode.SendToAddress(invoiceAddress, Money.Coins(1m));
TestUtils.Eventually(() =>
@ -204,9 +196,9 @@ namespace BTCPayServer.Tests
var psbt = wallet.CreatePSBT(btcNetwork, onchainBTC,
new WalletSendModel()
{
Outputs = new List<WalletSendModel.TransactionOutput>
Outputs = new List<WalletSendModel.TransactionOutput>()
{
new WalletSendModel.TransactionOutput
new WalletSendModel.TransactionOutput()
{
Amount = 0.5m,
DestinationAddress = new Key().PubKey.GetAddress(ScriptPubKeyType.Legacy, btcNetwork.NBitcoinNetwork)
@ -312,7 +304,7 @@ namespace BTCPayServer.Tests
var controller = tester.PayTester.GetController<InvoiceController>(null);
var checkout =
(Models.InvoicingModels.PaymentModel)((JsonResult)controller.GetStatus(invoice.Id)
(Models.InvoicingModels.PaymentModel)((JsonResult)controller.GetStatus(invoice.Id, null)
.GetAwaiter().GetResult()).Value;
Assert.Single(checkout.AvailableCryptos);
Assert.Equal("LTC", checkout.CryptoCode);
@ -329,7 +321,7 @@ namespace BTCPayServer.Tests
{
invoice = user.BitPay.GetInvoice(invoice.Id);
Assert.Equal("paid", invoice.Status);
checkout = (Models.InvoicingModels.PaymentModel)((JsonResult)controller.GetStatus(invoice.Id)
checkout = (Models.InvoicingModels.PaymentModel)((JsonResult)controller.GetStatus(invoice.Id, null)
.GetAwaiter().GetResult()).Value;
Assert.Equal("paid", checkout.Status);
});

@ -53,8 +53,4 @@
<ProjectReference Include="..\BTCPayServer\BTCPayServer.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Pages" />
</ItemGroup>
</Project>

@ -167,7 +167,6 @@ namespace BTCPayServer.Tests
l.SetMinimumLevel(LogLevel.Information)
.AddFilter("Microsoft", LogLevel.Error)
.AddFilter("Hangfire", LogLevel.Error)
.AddFilter("Fido2NetLib.DistributedCacheMetadataService", LogLevel.Error)
.AddProvider(Logs.LogProvider);
});
})

@ -0,0 +1,91 @@
using System.Threading.Tasks;
using BTCPayServer.Controllers;
using BTCPayServer.Data;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments.CoinSwitch;
using BTCPayServer.Tests.Logging;
using Microsoft.AspNetCore.Mvc;
using Xunit;
using Xunit.Abstractions;
namespace BTCPayServer.Tests
{
public class CoinSwitchTests
{
public CoinSwitchTests(ITestOutputHelper helper)
{
Logs.Tester = new XUnitLog(helper) { Name = "Tests" };
Logs.LogProvider = new XUnitLogProvider(helper);
}
[Fact]
[Trait("Integration", "Integration")]
public async Task CanSetCoinSwitchPaymentMethod()
{
using (var tester = ServerTester.Create())
{
await tester.StartAsync();
var user = tester.NewAccount();
user.GrantAccess();
var controller = tester.PayTester.GetController<StoresController>(user.UserId, user.StoreId);
var storeBlob = controller.CurrentStore.GetStoreBlob();
Assert.Null(storeBlob.CoinSwitchSettings);
var updateModel = new UpdateCoinSwitchSettingsViewModel()
{
MerchantId = "aaa",
};
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
await controller.UpdateCoinSwitchSettings(user.StoreId, updateModel, "save")).ActionName);
var store = await tester.PayTester.StoreRepository.FindStore(user.StoreId);
storeBlob = controller.CurrentStore.GetStoreBlob();
Assert.NotNull(storeBlob.CoinSwitchSettings);
Assert.NotNull(storeBlob.CoinSwitchSettings);
Assert.IsType<CoinSwitchSettings>(storeBlob.CoinSwitchSettings);
Assert.Equal(storeBlob.CoinSwitchSettings.MerchantId,
updateModel.MerchantId);
}
}
[Fact]
[Trait("Integration", "Integration")]
public async Task CanToggleCoinSwitchPaymentMethod()
{
using (var tester = ServerTester.Create())
{
await tester.StartAsync();
var user = tester.NewAccount();
user.GrantAccess();
var controller = tester.PayTester.GetController<StoresController>(user.UserId, user.StoreId);
var updateModel = new UpdateCoinSwitchSettingsViewModel()
{
MerchantId = "aaa",
Enabled = true
};
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
await controller.UpdateCoinSwitchSettings(user.StoreId, updateModel, "save")).ActionName);
var store = await tester.PayTester.StoreRepository.FindStore(user.StoreId);
Assert.True(store.GetStoreBlob().CoinSwitchSettings.Enabled);
updateModel.Enabled = false;
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
await controller.UpdateCoinSwitchSettings(user.StoreId, updateModel, "save")).ActionName);
store = await tester.PayTester.StoreRepository.FindStore(user.StoreId);
Assert.False(store.GetStoreBlob().CoinSwitchSettings.Enabled);
}
}
}
}

@ -23,7 +23,7 @@ namespace BTCPayServer.Tests
Logs.LogProvider = new XUnitLogProvider(helper);
}
[Fact(Timeout = LongRunningTestTimeout)]
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanCreateAndDeleteCrowdfundApp()
{
@ -63,7 +63,7 @@ namespace BTCPayServer.Tests
[Fact(Timeout = LongRunningTestTimeout)]
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanContributeOnlyWhenAllowed()
{
@ -155,7 +155,7 @@ namespace BTCPayServer.Tests
}
}
[Fact(Timeout = LongRunningTestTimeout)]
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanComputeCrowdfundModel()
{

@ -471,25 +471,6 @@ namespace BTCPayServer.Tests
{
Revision = payout.Revision
}));
// Create one pull payment with an amount of 9 decimals
var test3 = await client.CreatePullPayment(storeId, new Client.Models.CreatePullPaymentRequest()
{
Name = "Test 2",
Amount = 12.303228134m,
Currency = "BTC",
PaymentMethods = new[] { "BTC" }
});
destination = (await tester.ExplorerNode.GetNewAddressAsync()).ToString();
payout = await unauthenticated.CreatePayout(test3.Id, new CreatePayoutRequest()
{
Destination = destination,
PaymentMethod = "BTC"
});
payout = await client.ApprovePayout(storeId, payout.Id, new ApprovePayoutRequest());
// The payout should round the value of the payment down to the network of the payment method
Assert.Equal(12.30322814m, payout.PaymentMethodAmount);
Assert.Equal(12.303228134m, payout.Amount);
}
}
@ -706,7 +687,7 @@ namespace BTCPayServer.Tests
Assert.NotNull(newDelivery);
Assert.Equal(404, newDelivery.HttpCode);
var req = await clientProfile.GetWebhookDeliveryRequest(user.StoreId, hook.Id, newDeliveryId);
Assert.Equal(delivery.Id, req.OriginalDeliveryId);
Assert.Equal(delivery.Id, req.OrignalDeliveryId);
Assert.True(req.IsRedelivery);
Assert.Equal(WebhookDeliveryStatus.HttpError, newDelivery.Status);
});
@ -970,7 +951,7 @@ namespace BTCPayServer.Tests
});
await user.RegisterDerivationSchemeAsync("BTC");
var newInvoice = await client.CreateInvoice(user.StoreId,
new CreateInvoiceRequest() { Currency = "USD", Amount = 1, Metadata = JObject.Parse("{\"itemCode\": \"testitem\", \"orderId\": \"testOrder\"}"), Checkout = new CreateInvoiceRequest.CheckoutOptions()
new CreateInvoiceRequest() { Currency = "USD", Amount = 1, Metadata = JObject.Parse("{\"itemCode\": \"testitem\"}"), Checkout = new CreateInvoiceRequest.CheckoutOptions()
{
RedirectAutomatically = true
}});
@ -983,62 +964,6 @@ namespace BTCPayServer.Tests
Assert.Single(invoices);
Assert.Equal(newInvoice.Id, invoices.First().Id);
//list Filtered
var invoicesFiltered = await viewOnly.GetInvoices(user.StoreId,
orderId: null, status: null, DateTimeOffset.Now.AddHours(-1),
DateTimeOffset.Now.AddHours(1));
Assert.NotNull(invoicesFiltered);
Assert.Single(invoicesFiltered);
Assert.Equal(newInvoice.Id, invoicesFiltered.First().Id);
//list Yesterday
var invoicesYesterday = await viewOnly.GetInvoices(user.StoreId,
orderId: null, status: null, DateTimeOffset.Now.AddDays(-2),
DateTimeOffset.Now.AddDays(-1));
Assert.NotNull(invoicesYesterday);
Assert.Empty(invoicesYesterday);
// Error, startDate and endDate inverted
await AssertValidationError(new[] { "startDate", "endDate" },
() => viewOnly.GetInvoices(user.StoreId,
orderId: null, status: null, DateTimeOffset.Now.AddDays(-1),
DateTimeOffset.Now.AddDays(-2)));
await AssertValidationError(new[] { "startDate" },
() => viewOnly.SendHttpRequest<Client.Models.InvoiceData[]>($"api/v1/stores/{user.StoreId}/invoices", new Dictionary<string, object>()
{
{ "startDate", "blah" }
}));
//list Existing OrderId
var invoicesExistingOrderId =
await viewOnly.GetInvoices(user.StoreId, orderId: newInvoice.Metadata["orderId"].ToString());
Assert.NotNull(invoicesExistingOrderId);
Assert.Single(invoicesFiltered);
Assert.Equal(newInvoice.Id, invoicesFiltered.First().Id);
//list NonExisting OrderId
var invoicesNonExistingOrderId =
await viewOnly.GetInvoices(user.StoreId, orderId: "NonExistingOrderId");
Assert.NotNull(invoicesNonExistingOrderId);
Assert.Empty(invoicesNonExistingOrderId);
//list Existing Status
var invoicesExistingStatus =
await viewOnly.GetInvoices(user.StoreId, status:new []{newInvoice.Status});
Assert.NotNull(invoicesExistingStatus);
Assert.Single(invoicesExistingStatus);
Assert.Equal(newInvoice.Id, invoicesExistingStatus.First().Id);
//list NonExisting Status
var invoicesNonExistingStatus = await viewOnly.GetInvoices(user.StoreId,
status: new []{BTCPayServer.Client.Models.InvoiceStatus.Invalid});
Assert.NotNull(invoicesNonExistingStatus);
Assert.Empty(invoicesNonExistingStatus);
//get
var invoice = await viewOnly.GetInvoice(user.StoreId, newInvoice.Id);
Assert.Equal(newInvoice.Metadata, invoice.Metadata);

@ -320,7 +320,7 @@ namespace BTCPayServer.Tests
await TestUtils.EventuallyAsync(async () =>
{
var invoice = await invoiceRepository.GetInvoice(invoiceId);
var payments = invoice.GetPayments(false);
var payments = invoice.GetPayments();
Assert.Equal(2, payments.Count);
var originalPayment = payments[0];
var coinjoinPayment = payments[1];
@ -1088,7 +1088,7 @@ retry:
{
var invoiceEntity = await tester.PayTester.GetService<InvoiceRepository>().GetInvoice(invoice7.Id);
Assert.Equal(InvoiceStatusLegacy.Paid, invoiceEntity.Status);
Assert.Contains(invoiceEntity.GetPayments(false), p => p.Accounted &&
Assert.Contains(invoiceEntity.GetPayments(), p => p.Accounted &&
((BitcoinLikePaymentData)p.GetCryptoPaymentData()).PayjoinInformation is null);
});
////Assert.Contains(receiverWalletPayJoinState.GetRecords(), item => item.InvoiceId == invoice7.Id && item.TxSeen);
@ -1117,8 +1117,8 @@ retry:
{
var invoiceEntity = await tester.PayTester.GetService<InvoiceRepository>().GetInvoice(invoice7.Id);
Assert.Equal(InvoiceStatusLegacy.New, invoiceEntity.Status);
Assert.True(invoiceEntity.GetPayments(false).All(p => !p.Accounted));
ourOutpoint = invoiceEntity.GetAllBitcoinPaymentData(false).First().PayjoinInformation.ContributedOutPoints[0];
Assert.True(invoiceEntity.GetPayments().All(p => !p.Accounted));
ourOutpoint = invoiceEntity.GetAllBitcoinPaymentData().First().PayjoinInformation.ContributedOutPoints[0];
});
var payjoinRepository = tester.PayTester.GetService<PayJoinRepository>();
// The outpoint should now be available for next pj selection

@ -63,12 +63,7 @@ namespace BTCPayServer.Tests
}
options.AddArguments($"window-size={windowSize.Width}x{windowSize.Height}");
options.AddArgument("shm-size=2g");
var cds = ChromeDriverService.CreateDefaultService(chromeDriverPath);
cds.Port = Utils.FreeTcpPort();
cds.HostName = "127.0.0.1";
cds.Start();
Driver = new ChromeDriver(cds, options,
Driver = new ChromeDriver(chromeDriverPath, options,
// A bit less than test timeout
TimeSpan.FromSeconds(50));
@ -217,25 +212,20 @@ namespace BTCPayServer.Tests
{
Driver.FindElement(By.CssSelector("label[for=\"LightningNodeType-Custom\"]")).Click();
Driver.FindElement(By.Id("ConnectionString")).SendKeys(connectionString);
Driver.FindElement(By.Id("test")).Click();
Assert.Contains("Connection to the Lightning node successful.", FindAlertMessage().Text);
}
var enabled = Driver.FindElement(By.Id("Enabled"));
if (!enabled.Selected) enabled.Click();
Driver.FindElement(By.Id("test")).Click();
Assert.Contains("Connection to the Lightning node succeeded.", FindAlertMessage().Text);
Driver.FindElement(By.Id("save")).Click();
Assert.Contains($"{cryptoCode} Lightning node updated.", FindAlertMessage().Text);
var enabled = Driver.FindElement(By.Id($"{cryptoCode}LightningEnabled"));
if (enabled.Text == "Enable")
{
enabled.Click();
Assert.Contains($"{cryptoCode} Lightning payments are now enabled for this store.", FindAlertMessage().Text);
}
}
public void ClickOnAllSideMenus()
{
var links = Driver.FindElements(By.CssSelector(".nav .nav-link")).Select(c => c.GetAttribute("href")).ToList();
var links = Driver.FindElements(By.CssSelector(".nav-pills .nav-link")).Select(c => c.GetAttribute("href")).ToList();
Driver.AssertNoError();
Assert.NotEmpty(links);
foreach (var l in links)
@ -378,14 +368,14 @@ namespace BTCPayServer.Tests
private void CheckForJSErrors()
{
//wait for seleniun update: https://stackoverflow.com/questions/57520296/selenium-webdriver-3-141-0-driver-manage-logs-availablelogtypes-throwing-syste
// var errorStrings = new List<string>
// {
// "SyntaxError",
// "EvalError",
// "ReferenceError",
// "RangeError",
// "TypeError",
// "URIError"
// var errorStrings = new List<string>
// {
// "SyntaxError",
// "EvalError",
// "ReferenceError",
// "RangeError",
// "TypeError",
// "URIError"
// };
//
// var jsErrors = Driver.Manage().Logs.GetLog(LogType.Browser).Where(x => errorStrings.Any(e => x.Message.Contains(e)));
@ -412,7 +402,7 @@ namespace BTCPayServer.Tests
{
Driver.Navigate().GoToUrl(new Uri(Server.PayTester.ServerUri, relativeUrl));
}
public void GoToServer(ServerNavPages navPages = ServerNavPages.Index)
{
Driver.FindElement(By.Id("ServerSettings")).Click();

@ -1,5 +1,4 @@
using System;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
using System.Text;
@ -8,7 +7,6 @@ using System.Threading.Tasks;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.Services;
using BTCPayServer.Services.Wallets;
using BTCPayServer.Tests.Logging;
using BTCPayServer.Views.Manage;
@ -214,10 +212,6 @@ namespace BTCPayServer.Tests
using (var s = SeleniumTester.Create())
{
await s.StartAsync();
var settings = s.Server.PayTester.GetService<SettingsRepository>();
var policies = await settings.GetSettingAsync<PoliciesSettings>() ?? new PoliciesSettings();
policies.DisableSSHService = false;
await settings.UpdateSetting(policies);
s.RegisterNewUser(isAdmin: true);
s.Driver.Navigate().GoToUrl(s.Link("/server/services"));
Assert.Contains("server/services/ssh", s.Driver.PageSource);
@ -246,17 +240,6 @@ namespace BTCPayServer.Tests
text = s.Driver.FindElement(By.Id("SSHKeyFileContent")).Text;
Assert.DoesNotContain("test2", text);
// Let's try to disable it now
s.Driver.FindElement(By.Id("disable")).Click();
s.Driver.FindElement(By.Id("continue")).Click();
policies = await settings.GetSettingAsync<PoliciesSettings>();
Assert.True(policies.DisableSSHService);
s.Driver.Navigate().GoToUrl(s.Link("/server/services/ssh"));
Assert.True(s.Driver.PageSource.Contains("404 - Page not found", StringComparison.OrdinalIgnoreCase));
policies.DisableSSHService = false;
await settings.UpdateSetting(policies);
}
}
@ -366,7 +349,7 @@ namespace BTCPayServer.Tests
s.AddLightningNode();
s.Driver.AssertNoError();
var successAlert = s.FindAlertMessage();
Assert.Contains("BTC Lightning node updated.", successAlert.Text);
Assert.Contains("BTC Lightning node modified.", successAlert.Text);
Assert.False(s.Driver.PageSource.Contains(offchainHint), "Lightning hint should be dismissed at this point");
var storeUrl = s.Driver.Url;
@ -981,18 +964,15 @@ namespace BTCPayServer.Tests
var payouts = s.Driver.FindElements(By.ClassName("pp-payout"));
Assert.Equal(2, payouts.Count);
payouts[1].Click();
Assert.Empty(s.Driver.FindElements(By.ClassName("payout")));
Assert.Contains("No payout waiting for approval", s.Driver.PageSource);
// PP2 should have payouts
s.GoToWallet(navPages: WalletsNavPages.PullPayments);
payouts = s.Driver.FindElements(By.ClassName("pp-payout"));
payouts[0].Click();
Assert.NotEmpty(s.Driver.FindElements(By.ClassName("payout")));
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-selectAllCheckbox")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-actions")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-approve-pay")).Click();
Assert.DoesNotContain("No payout waiting for approval", s.Driver.PageSource);
s.Driver.FindElement(By.Id("selectAllCheckbox")).Click();
s.Driver.FindElement(By.Id("payCommand")).Click();
s.Driver.FindElement(By.Id("SendMenu")).Click();
s.Driver.FindElement(By.CssSelector("button[value=nbx-seed]")).Click();
s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).Click();
@ -1007,14 +987,13 @@ namespace BTCPayServer.Tests
Assert.Equal("payout", s.Driver.FindElement(By.ClassName("transactionLabel")).Text);
s.GoToWallet(navPages: WalletsNavPages.Payouts);
ReadOnlyCollection<IWebElement> txs;
TestUtils.Eventually(() =>
{
s.Driver.Navigate().Refresh();
txs = s.Driver.FindElements(By.ClassName("transaction-link"));
Assert.Equal(2, txs.Count);
Assert.Contains("No payout waiting for approval", s.Driver.PageSource);
});
var txs = s.Driver.FindElements(By.ClassName("transaction-link"));
Assert.Equal(2, txs.Count);
s.Driver.Navigate().GoToUrl(viewPullPaymentUrl);
txs = s.Driver.FindElements(By.ClassName("transaction-link"));
@ -1035,7 +1014,7 @@ namespace BTCPayServer.Tests
{
using var ctx = s.Server.PayTester.GetService<ApplicationDbContextFactory>().CreateContext();
var payoutsData = await ctx.Payouts.Where(p => p.PullPaymentDataId == pullPaymentId).ToListAsync();
Assert.True(payoutsData.All(p => p.State == PayoutState.Completed));
Assert.True(payoutsData.All(p => p.State == Data.PayoutState.Completed));
});
}
}

@ -273,8 +273,8 @@ namespace BTCPayServer.Tests
var connectionString = parent.GetLightningConnectionString(connectionType, isMerchant);
var nodeType = connectionString == LightningSupportedPaymentMethod.InternalNode ? LightningNodeType.Internal : LightningNodeType.Custom;
await storeController.SetupLightningNode(storeId ?? StoreId,
new LightningNodeViewModel { ConnectionString = connectionString, LightningNodeType = nodeType, SkipPortTest = true }, "save", cryptoCode);
await storeController.AddLightningNode(storeId ?? StoreId,
new LightningNodeViewModel { ConnectionString = connectionString, LightningNodeType = nodeType, SkipPortTest = true }, "save", "BTC");
if (storeController.ModelState.ErrorCount != 0)
Assert.False(true, storeController.ModelState.FirstOrDefault().Value.Errors[0].ErrorMessage);
}

@ -0,0 +1,132 @@
using System;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Controllers;
using BTCPayServer.Data;
using BTCPayServer.Models.AccountViewModels;
using BTCPayServer.Tests.Logging;
using BTCPayServer.U2F;
using BTCPayServer.U2F.Models;
using Microsoft.AspNetCore.Mvc;
using U2F.Core.Models;
using U2F.Core.Utils;
using Xunit;
using Xunit.Abstractions;
namespace BTCPayServer.Tests
{
public class U2FTests
{
public const int TestTimeout = 60_000;
public U2FTests(ITestOutputHelper helper)
{
Logs.Tester = new XUnitLog(helper) { Name = "Tests" };
Logs.LogProvider = new XUnitLogProvider(helper);
}
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task U2ftest()
{
using (var tester = ServerTester.Create())
{
await tester.StartAsync();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
var accountController = tester.PayTester.GetController<AccountController>();
var manageController = user.GetController<ManageController>();
var mock = new MockU2FService(tester.PayTester.GetService<ApplicationDbContextFactory>());
manageController._u2FService = mock;
accountController._u2FService = mock;
Assert
.IsType<RedirectToActionResult>(await accountController.Login(new LoginViewModel()
{
Email = user.RegisterDetails.Email,
Password = user.RegisterDetails.Password
}));
Assert.Empty(Assert.IsType<U2FAuthenticationViewModel>(Assert
.IsType<ViewResult>(await manageController.U2FAuthentication()).Model).Devices);
var addDeviceVM = Assert.IsType<AddU2FDeviceViewModel>(Assert
.IsType<ViewResult>(manageController.AddU2FDevice("testdevice")).Model);
Assert.NotEmpty(addDeviceVM.Challenge);
Assert.Equal("testdevice", addDeviceVM.Name);
Assert.NotEmpty(addDeviceVM.Version);
Assert.Null(addDeviceVM.DeviceResponse);
var devReg = new DeviceRegistration(Guid.NewGuid().ToByteArray(), Guid.NewGuid().ToByteArray(),
Guid.NewGuid().ToByteArray(), 1);
mock.GetDevReg = () => devReg;
mock.StartedAuthentication = () =>
new StartedAuthentication("chocolate", addDeviceVM.AppId,
devReg.KeyHandle.ByteArrayToBase64String());
addDeviceVM.DeviceResponse = new RegisterResponse("ss",
Convert.ToBase64String(Encoding.UTF8.GetBytes("{typ:'x', challenge: 'fff'}"))).ToJson();
Assert
.IsType<RedirectToActionResult>(await manageController.AddU2FDevice(addDeviceVM));
Assert.Single(Assert.IsType<U2FAuthenticationViewModel>(Assert
.IsType<ViewResult>(await manageController.U2FAuthentication()).Model).Devices);
var secondaryLoginViewModel = Assert.IsType<SecondaryLoginViewModel>(Assert
.IsType<ViewResult>(await accountController.Login(new LoginViewModel()
{
Email = user.RegisterDetails.Email,
Password = user.RegisterDetails.Password
})).Model);
Assert.NotNull(secondaryLoginViewModel.LoginWithU2FViewModel);
Assert.Single(secondaryLoginViewModel.LoginWithU2FViewModel.Challenges);
Assert.Equal(secondaryLoginViewModel.LoginWithU2FViewModel.Challenge,
secondaryLoginViewModel.LoginWithU2FViewModel.Challenges.First().challenge);
secondaryLoginViewModel.LoginWithU2FViewModel.DeviceResponse = new AuthenticateResponse(
Convert.ToBase64String(Encoding.UTF8.GetBytes(
"{typ:'x', challenge: '" + secondaryLoginViewModel.LoginWithU2FViewModel.Challenge + "'}")),
"dd", devReg.KeyHandle.ByteArrayToBase64String()).ToJson();
Assert
.IsType<RedirectToActionResult>(
await accountController.LoginWithU2F(secondaryLoginViewModel.LoginWithU2FViewModel));
}
}
public class MockU2FService : U2FService
{
public Func<DeviceRegistration> GetDevReg;
public Func<StartedAuthentication> StartedAuthentication;
public MockU2FService(ApplicationDbContextFactory contextFactory) : base(contextFactory)
{
}
protected override StartedRegistration StartDeviceRegistrationCore(string appId)
{
return global::U2F.Core.Crypto.U2F.StartRegistration(appId);
}
protected override DeviceRegistration FinishRegistrationCore(StartedRegistration startedRegistration,
RegisterResponse registerResponse)
{
return GetDevReg();
}
protected override StartedAuthentication StartAuthenticationCore(string appId, U2FDevice registeredDevice)
{
return StartedAuthentication();
}
protected override void FinishAuthenticationCore(StartedAuthentication authentication,
AuthenticateResponse authenticateResponse, DeviceRegistration registration)
{
}
}
}
}

@ -19,8 +19,6 @@ using BTCPayServer.Configuration;
using BTCPayServer.Controllers;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.Fido2;
using BTCPayServer.Fido2.Models;
using BTCPayServer.HostedServices;
using BTCPayServer.Hosting;
using BTCPayServer.Lightning;
@ -44,15 +42,12 @@ using BTCPayServer.Services.Labels;
using BTCPayServer.Services.Mails;
using BTCPayServer.Services.Rates;
using BTCPayServer.Tests.Logging;
using BTCPayServer.U2F.Models;
using BTCPayServer.Validation;
using ExchangeSharp;
using Fido2NetLib;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.Memory;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using NBitcoin;
using NBitcoin.DataEncoders;
using NBitcoin.Payment;
@ -72,7 +67,7 @@ namespace BTCPayServer.Tests
{
public class UnitTest1
{
public const int LongRunningTestTimeout = 60_000; // 60s
public const int TestTimeout = 60_000;
public UnitTest1(ITestOutputHelper helper)
{
@ -875,55 +870,18 @@ namespace BTCPayServer.Tests
}
}
[Fact(Timeout = LongRunningTestTimeout)]
[Fact]
[Trait("Fast", "Fast")]
public async Task CanEnumerateTorServices()
{
var tor = new TorServices(new BTCPayNetworkProvider(ChainName.Regtest),
new OptionsWrapper<BTCPayServerOptions>(new BTCPayServerOptions()
{
TorrcFile = TestUtils.GetTestDataFullPath("Tor/torrc")
}));
new BTCPayServerOptions() { TorrcFile = TestUtils.GetTestDataFullPath("Tor/torrc") });
await tor.Refresh();
Assert.Single(tor.Services.Where(t => t.ServiceType == TorServiceType.BTCPayServer));
Assert.Single(tor.Services.Where(t => t.ServiceType == TorServiceType.P2P));
Assert.Single(tor.Services.Where(t => t.ServiceType == TorServiceType.RPC));
Assert.True(tor.Services.Count(t => t.ServiceType == TorServiceType.Other) > 1);
tor = new TorServices(new BTCPayNetworkProvider(ChainName.Regtest),
new OptionsWrapper<BTCPayServerOptions>(new BTCPayServerOptions()
{
TorrcFile = null,
TorServices = "btcpayserver:host.onion:80;btc-p2p:host2.onion:81,BTC-RPC:host3.onion:82,UNKNOWN:host4.onion:83,INVALID:ddd".Split(new[] {';', ','}, StringSplitOptions.RemoveEmptyEntries)
}));
await Task.WhenAll(tor.StartAsync(CancellationToken.None));
var btcpayS = Assert.Single(tor.Services.Where(t => t.ServiceType == TorServiceType.BTCPayServer));
Assert.Null(btcpayS.Network);
Assert.Equal("host.onion", btcpayS.OnionHost);
Assert.Equal(80, btcpayS.VirtualPort);
var p2p = Assert.Single(tor.Services.Where(t => t.ServiceType == TorServiceType.P2P));
Assert.NotNull(p2p.Network);
Assert.Equal("BTC", p2p.Network.CryptoCode);
Assert.Equal("host2.onion", p2p.OnionHost);
Assert.Equal(81, p2p.VirtualPort);
var rpc = Assert.Single(tor.Services.Where(t => t.ServiceType == TorServiceType.RPC));
Assert.NotNull(p2p.Network);
Assert.Equal("BTC", rpc.Network.CryptoCode);
Assert.Equal("host3.onion", rpc.OnionHost);
Assert.Equal(82, rpc.VirtualPort);
var unknown = Assert.Single(tor.Services.Where(t => t.ServiceType == TorServiceType.Other));
Assert.Null(unknown.Network);
Assert.Equal("host4.onion", unknown.OnionHost);
Assert.Equal(83, unknown.VirtualPort);
Assert.Equal("UNKNOWN", unknown.Name);
Assert.Equal(4, tor.Services.Length);
Assert.True(tor.Services.Where(t => t.ServiceType == TorServiceType.Other).Count() > 1);
}
@ -990,9 +948,9 @@ namespace BTCPayServer.Tests
user.GrantAccess(true);
var storeController = user.GetController<StoresController>();
Assert.IsType<ViewResult>(storeController.UpdateStore());
Assert.IsType<ViewResult>(storeController.SetupLightningNode(user.StoreId, "BTC"));
Assert.IsType<ViewResult>(storeController.AddLightningNode(user.StoreId, "BTC"));
var testResult = storeController.SetupLightningNode(user.StoreId, new LightningNodeViewModel
var testResult = storeController.AddLightningNode(user.StoreId, new LightningNodeViewModel()
{
ConnectionString = $"type=charge;server={tester.MerchantCharge.Client.Uri.AbsoluteUri};allowinsecure=true",
SkipPortTest = true // We can't test this as the IP can't be resolved by the test host :(
@ -1001,19 +959,19 @@ namespace BTCPayServer.Tests
storeController.TempData.Clear();
Assert.True(storeController.ModelState.IsValid);
Assert.IsType<RedirectToActionResult>(storeController.SetupLightningNode(user.StoreId,
new LightningNodeViewModel
Assert.IsType<RedirectToActionResult>(storeController.AddLightningNode(user.StoreId,
new LightningNodeViewModel()
{
ConnectionString = $"type=charge;server={tester.MerchantCharge.Client.Uri.AbsoluteUri};allowinsecure=true"
}, "save", "BTC").GetAwaiter().GetResult());
// Make sure old connection string format does not work
Assert.IsType<RedirectToActionResult>(storeController.SetupLightningNode(user.StoreId,
new LightningNodeViewModel { ConnectionString = tester.MerchantCharge.Client.Uri.AbsoluteUri },
Assert.IsType<ViewResult>(storeController.AddLightningNode(user.StoreId,
new LightningNodeViewModel() { ConnectionString = tester.MerchantCharge.Client.Uri.AbsoluteUri },
"save", "BTC").GetAwaiter().GetResult());
var storeVm =
Assert.IsType<StoreViewModel>(Assert
Assert.IsType<Models.StoreViewModels.StoreViewModel>(Assert
.IsType<ViewResult>(storeController.UpdateStore()).Model);
Assert.Single(storeVm.LightningNodes.Where(l => !string.IsNullOrEmpty(l.Address)));
}
@ -1088,7 +1046,7 @@ namespace BTCPayServer.Tests
});
}
[Fact(Timeout = LongRunningTestTimeout)]
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanUseServerInitiatedPairingCode()
{
@ -1115,7 +1073,7 @@ namespace BTCPayServer.Tests
}
}
[Fact(Timeout = LongRunningTestTimeout)]
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanSendIPN()
{
@ -1185,7 +1143,7 @@ namespace BTCPayServer.Tests
}
}
[Fact(Timeout = LongRunningTestTimeout)]
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CantPairTwiceWithSamePubkey()
{
@ -1209,7 +1167,7 @@ namespace BTCPayServer.Tests
}
}
[Fact(Timeout = LongRunningTestTimeout)]
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public void CanSolveTheDogesRatesOnKraken()
{
@ -1227,7 +1185,7 @@ namespace BTCPayServer.Tests
}
[Fact(Timeout = LongRunningTestTimeout)]
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanUseTorClient()
{
@ -1280,7 +1238,7 @@ namespace BTCPayServer.Tests
}
}
[Fact(Timeout = LongRunningTestTimeout)]
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanRescanWallet()
{
@ -1382,7 +1340,7 @@ namespace BTCPayServer.Tests
}
}
[Fact(Timeout = LongRunningTestTimeout)]
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanListInvoices()
{
@ -1433,7 +1391,7 @@ namespace BTCPayServer.Tests
}
}
[Fact(Timeout = LongRunningTestTimeout)]
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanListNotifications()
{
@ -1639,8 +1597,8 @@ namespace BTCPayServer.Tests
{
var i = await tester.PayTester.InvoiceRepository.GetInvoice(invoice2.Id);
Assert.Equal(InvoiceStatusLegacy.New, i.Status);
Assert.Single(i.GetPayments(false));
Assert.False(i.GetPayments(false).First().Accounted);
Assert.Single(i.GetPayments());
Assert.False(i.GetPayments().First().Accounted);
});
Logs.Tester.LogInformation(
@ -1672,8 +1630,8 @@ namespace BTCPayServer.Tests
await TestUtils.EventuallyAsync(async () =>
{
var invoiceEntity = await tester.PayTester.InvoiceRepository.GetInvoice(invoice.Id);
var btcPayments = invoiceEntity.GetAllBitcoinPaymentData(false).ToArray();
var payments = invoiceEntity.GetPayments(false).ToArray();
var btcPayments = invoiceEntity.GetAllBitcoinPaymentData().ToArray();
var payments = invoiceEntity.GetPayments().ToArray();
Assert.Equal(tx1, btcPayments[0].Outpoint.Hash);
Assert.False(payments[0].Accounted);
Assert.Equal(tx1Bump, payments[1].Outpoint.Hash);
@ -1717,7 +1675,7 @@ namespace BTCPayServer.Tests
Assert.NotNull(paymentData.KeyPath);
}
[Fact(Timeout = LongRunningTestTimeout)]
[Fact(Timeout = TestTimeout)]
[Trait("Fast", "Fast")]
public void CanParseFilter()
{
@ -1745,7 +1703,7 @@ namespace BTCPayServer.Tests
Assert.Equal("hekki", search.TextSearch);
}
[Fact(Timeout = LongRunningTestTimeout)]
[Fact(Timeout = TestTimeout)]
[Trait("Fast", "Fast")]
public void CanParseFingerprint()
{
@ -1762,7 +1720,7 @@ namespace BTCPayServer.Tests
Assert.Equal(f1.ToString(), f2.ToString());
}
[Fact(Timeout = LongRunningTestTimeout)]
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async void CheckCORSSetOnBitpayAPI()
{
@ -1797,7 +1755,7 @@ namespace BTCPayServer.Tests
}
}
[Fact(Timeout = LongRunningTestTimeout)]
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task TestAccessBitpayAPI()
{
@ -1876,7 +1834,7 @@ namespace BTCPayServer.Tests
}
}
[Fact(Timeout = LongRunningTestTimeout)]
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanUseExchangeSpecificRate()
{
@ -1921,7 +1879,7 @@ namespace BTCPayServer.Tests
return invoice2.CryptoInfo[0].Rate;
}
[Fact(Timeout = LongRunningTestTimeout)]
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanUseAnyoneCanCreateInvoice()
{
@ -1973,7 +1931,7 @@ namespace BTCPayServer.Tests
}
}
[Fact(Timeout = LongRunningTestTimeout)]
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanTweakRate()
{
@ -2020,7 +1978,7 @@ namespace BTCPayServer.Tests
}
}
[Fact(Timeout = LongRunningTestTimeout)]
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanModifyRates()
{
@ -2349,7 +2307,7 @@ namespace BTCPayServer.Tests
Assert.True(client.WaitAllRunning(default).Wait(100));
}
[Fact(Timeout = LongRunningTestTimeout)]
[Fact(Timeout = TestTimeout)]
[Trait("Fast", "Fast")]
public void PosDataParser_ParsesCorrectly()
{
@ -2366,9 +2324,7 @@ namespace BTCPayServer.Tests
{
("{ invalidjson file here}",
new Dictionary<string, object>() {{String.Empty, "{ invalidjson file here}"}})
},
// Duplicate keys should not crash things
{("{ \"key\": true, \"key\": true}", new Dictionary<string, object>() {{"key", "True"}})}
}
};
testCases.ForEach(tuple =>
@ -2377,7 +2333,7 @@ namespace BTCPayServer.Tests
});
}
[Fact(Timeout = LongRunningTestTimeout)]
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task PosDataParser_ParsesCorrectly_Slower()
{
@ -2427,7 +2383,7 @@ namespace BTCPayServer.Tests
}
}
[Fact(Timeout = LongRunningTestTimeout)]
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanExportInvoicesJson()
{
@ -2506,7 +2462,7 @@ namespace BTCPayServer.Tests
}
}
[Fact(Timeout = LongRunningTestTimeout)]
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanChangeNetworkFeeMode()
{
@ -2597,7 +2553,7 @@ namespace BTCPayServer.Tests
}
}
[Fact(Timeout = LongRunningTestTimeout)]
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanExportInvoicesCsv()
{
@ -2639,7 +2595,7 @@ namespace BTCPayServer.Tests
}
[Fact(Timeout = LongRunningTestTimeout)]
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanCreateAndDeleteApps()
{
@ -2677,7 +2633,7 @@ namespace BTCPayServer.Tests
}
}
[Fact(Timeout = LongRunningTestTimeout)]
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanCreateStrangeInvoice()
{
@ -2723,7 +2679,7 @@ namespace BTCPayServer.Tests
}
}
[Fact(Timeout = LongRunningTestTimeout)]
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task InvoiceFlowThroughDifferentStatesCorrectly()
{
@ -2913,7 +2869,7 @@ namespace BTCPayServer.Tests
}
}
[Fact(Timeout = LongRunningTestTimeout)]
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public void CanQueryDirectProviders()
{
@ -2984,7 +2940,7 @@ namespace BTCPayServer.Tests
}
[Fact(Timeout = LongRunningTestTimeout)]
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanExportBackgroundFetcherState()
{
@ -3026,7 +2982,7 @@ namespace BTCPayServer.Tests
}
[Fact(Timeout = LongRunningTestTimeout)]
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public void CanGetRateCryptoCurrenciesByDefault()
{
@ -3078,7 +3034,7 @@ namespace BTCPayServer.Tests
}
}
[Fact(Timeout = LongRunningTestTimeout)]
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CheckLogsRoute()
{
@ -3161,7 +3117,7 @@ namespace BTCPayServer.Tests
return name;
}
[Fact(Timeout = LongRunningTestTimeout)]
[Fact(Timeout = TestTimeout)]
[Trait("Fast", "Fast")]
public void CanCheckFileNameValid()
{
@ -3179,19 +3135,7 @@ namespace BTCPayServer.Tests
}
}
[Trait("Fast", "Fast")]
[Fact]
public void CanFixupWebhookEventPropertyName()
{
string legacy = "{\"orignalDeliveryId\":\"blahblah\"}";
var obj = JsonConvert.DeserializeObject<WebhookEvent>(legacy, WebhookEvent.DefaultSerializerSettings);
Assert.Equal("blahblah", obj.OriginalDeliveryId);
var serialized = JsonConvert.SerializeObject(obj, WebhookEvent.DefaultSerializerSettings);
Assert.DoesNotContain("orignalDeliveryId", serialized);
Assert.Contains("originalDeliveryId", serialized);
}
[Fact(Timeout = LongRunningTestTimeout)]
[Fact(Timeout = TestTimeout)]
[Trait("Fast", "Fast")]
public async Task CanCreateSqlitedb()
{
@ -3203,7 +3147,7 @@ namespace BTCPayServer.Tests
await new ApplicationDbContext(builder.Options).Database.MigrateAsync();
}
[Fact(Timeout = LongRunningTestTimeout)]
[Fact(Timeout = TestTimeout)]
[Trait("Fast", "Fast")]
public void CanUsePermission()
{
@ -3228,7 +3172,7 @@ namespace BTCPayServer.Tests
.Contains(Permission.Create(Policies.CanModifyStoreSettings)));
}
[Fact(Timeout = LongRunningTestTimeout)]
[Fact(Timeout = TestTimeout)]
[Trait("Fast", "Fast")]
public void CheckRatesProvider()
{
@ -3316,7 +3260,7 @@ namespace BTCPayServer.Tests
}
[Fact(Timeout = LongRunningTestTimeout)]
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanLoginWithNoSecondaryAuthSystemsOrRequestItWhenAdded()
{
@ -3328,7 +3272,7 @@ namespace BTCPayServer.Tests
var accountController = tester.PayTester.GetController<AccountController>();
//no 2fa or fido2 enabled, login should work
//no 2fa or u2f enabled, login should work
Assert.Equal(nameof(HomeController.Index),
Assert.IsType<RedirectToActionResult>(await accountController.Login(new LoginViewModel()
{
@ -3336,46 +3280,48 @@ namespace BTCPayServer.Tests
Password = user.RegisterDetails.Password
})).ActionName);
var manageController = user.GetController<Fido2Controller>();
var manageController = user.GetController<ManageController>();
//by default no fido2 devices available
//by default no u2f devices available
Assert.Empty(Assert
.IsType<Fido2AuthenticationViewModel>(Assert
.IsType<ViewResult>(await manageController.List()).Model).Credentials);
Assert.IsType<CredentialCreateOptions>(Assert
.IsType<ViewResult>(await manageController.Create(new AddFido2CredentialViewModel()
{
Name = "label"
})).Model);
.IsType<U2FAuthenticationViewModel>(Assert
.IsType<ViewResult>(await manageController.U2FAuthentication()).Model).Devices);
var addRequest =
Assert.IsType<AddU2FDeviceViewModel>(Assert
.IsType<ViewResult>(manageController.AddU2FDevice("label")).Model);
//name should match the one provided in beginning
Assert.Equal("label", addRequest.Name);
//sending an invalid response model back to server, should error out
Assert.IsType<RedirectToActionResult>(await manageController.CreateResponse("sdsdsa", "sds"));
Assert.IsType<RedirectToActionResult>(await manageController.AddU2FDevice(addRequest));
var statusModel = manageController.TempData.GetStatusMessageModel();
Assert.Equal(StatusMessageModel.StatusSeverity.Error, statusModel.Severity);
var contextFactory = tester.PayTester.GetService<ApplicationDbContextFactory>();
//add a fake fido2 device in db directly since emulating a fido2 device is hard and annoying
//add a fake u2f device in db directly since emulating a u2f device is hard and annoying
using (var context = contextFactory.CreateContext())
{
var newDevice = new Fido2Credential()
var newDevice = new U2FDevice()
{
Id = Guid.NewGuid().ToString(),
Name = "fake",
Type = Fido2Credential.CredentialType.FIDO2,
Counter = 0,
KeyHandle = UTF8Encoding.UTF8.GetBytes("fake"),
PublicKey = UTF8Encoding.UTF8.GetBytes("fake"),
AttestationCert = UTF8Encoding.UTF8.GetBytes("fake"),
ApplicationUserId = user.UserId
};
newDevice.SetBlob(new Fido2CredentialBlob() { });
await context.Fido2Credentials.AddAsync(newDevice);
await context.U2FDevices.AddAsync(newDevice);
await context.SaveChangesAsync();
Assert.NotNull(newDevice.Id);
Assert.NotEmpty(Assert
.IsType<Fido2AuthenticationViewModel>(Assert
.IsType<ViewResult>(await manageController.List()).Model).Credentials);
.IsType<U2FAuthenticationViewModel>(Assert
.IsType<ViewResult>(await manageController.U2FAuthentication()).Model).Devices);
}
//check if we are showing the fido2 login screen now
//check if we are showing the u2f login screen now
var secondLoginResult = Assert.IsType<ViewResult>(await accountController.Login(new LoginViewModel()
{
Email = user.RegisterDetails.Email,
@ -3386,11 +3332,11 @@ namespace BTCPayServer.Tests
var vm = Assert.IsType<SecondaryLoginViewModel>(secondLoginResult.Model);
//2fa was never enabled for user so this should be empty
Assert.Null(vm.LoginWith2FaViewModel);
Assert.NotNull(vm.LoginWithFido2ViewModel);
Assert.NotNull(vm.LoginWithU2FViewModel);
}
}
[Fact(Timeout = LongRunningTestTimeout)]
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async void CheckOnionlocationForNonOnionHtmlRequests()
{
@ -3436,7 +3382,7 @@ namespace BTCPayServer.Tests
}
}
[Fact(Timeout = LongRunningTestTimeout)]
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanCheckForNewVersion()
{
@ -3482,7 +3428,7 @@ namespace BTCPayServer.Tests
}
}
[Fact(Timeout = LongRunningTestTimeout)]
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanDoLightningInternalNodeMigration()
{
@ -3561,7 +3507,7 @@ namespace BTCPayServer.Tests
}
[Fact(Timeout = LongRunningTestTimeout)]
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanDoInvoiceMigrations()
{
@ -3642,7 +3588,7 @@ namespace BTCPayServer.Tests
await migrationStartupTask.ExecuteAsync();
}
[Fact(Timeout = LongRunningTestTimeout)]
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task EmailSenderTests()
{

@ -51,8 +51,6 @@
<PackageReference Include="BundlerMinifier.Core" Version="3.2.435" />
<PackageReference Include="BundlerMinifier.TagHelpers" Version="3.2.435" />
<PackageReference Include="CsvHelper" Version="15.0.5" />
<PackageReference Include="Fido2" Version="2.0.1" />
<PackageReference Include="Fido2.AspNet" Version="2.0.1" />
<PackageReference Include="HtmlSanitizer" Version="5.0.372" />
<PackageReference Include="McMaster.NETCore.Plugins.Mvc" Version="1.3.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />
@ -84,6 +82,7 @@
<PackageReference Include="TwentyTwenty.Storage.Azure" Version="2.12.1" />
<PackageReference Include="TwentyTwenty.Storage.Google" Version="2.12.1" />
<PackageReference Include="TwentyTwenty.Storage.Local" Version="2.12.1" />
<PackageReference Include="U2F.Core" Version="1.0.4" />
<PackageReference Include="YamlDotNet" Version="8.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="3.1.1" Condition="'$(RazorCompileOnBuild)' != 'true'" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.1" />
@ -135,9 +134,11 @@
</ItemGroup>
<ItemGroup>
<Folder Include="Views\Stores\Integrations" />
<Folder Include="wwwroot\vendor\clipboard.js\" />
<Folder Include="wwwroot\vendor\highlightjs\" />
<Folder Include="wwwroot\vendor\summernote" />
<Folder Include="wwwroot\vendor\u2f" />
<Folder Include="wwwroot\vendor\vue-qrcode-reader" />
</ItemGroup>

@ -3,17 +3,17 @@
@inject CssThemeManager CssThemeManager
@using BTCPayServer.HostedServices
@using BTCPayServer.Views.Notifications
@using Microsoft.AspNetCore.Http.Extensions
@model BTCPayServer.Components.NotificationsDropdown.NotificationSummaryViewModel
@addTagHelper *, BundlerMinifier.TagHelpers
@if (Model.UnseenCount > 0)
{
<li class="nav-item dropdown" id="notifications-nav-item">
<a class="nav-link js-scroll-trigger @ViewData.IsActiveCategory(typeof(NotificationsNavPages))" href="#" id="navbarDropdown" role="button" data-toggle="dropdown">
<span class="d-inline-block d-lg-none">Notifications</span>
<i class="fa fa-bell d-lg-inline-block d-none"></i>
<span class="notification-badge badge badge-pill badge-danger">@Model.UnseenCount</span>
<a class="nav-link js-scroll-trigger @ViewData.IsActiveCategory(typeof(NotificationsNavPages))" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" id="Notifications">
<span class="d-lg-none d-sm-block">Notifications</span><i class="fa fa-bell d-lg-inline-block d-none"></i>
</a>
<span class="alerts-badge badge badge-pill badge-danger">@Model.UnseenCount</span>
<div class="dropdown-menu dropdown-menu-right text-center notification-dropdown" aria-labelledby="navbarDropdown">
<div class="d-flex align-items-center justify-content-between py-3 px-4 border-bottom border-light">
<h5 class="m-0">Notifications</h5>
@ -57,7 +57,7 @@ else
if (!disabled)
{
var user = await UserManager.GetUserAsync(User);
disabled = user?.DisabledNotifications == "all";
disabled = user.DisabledNotifications == "all";
}
}
@if (!disabled)

@ -69,12 +69,7 @@ namespace BTCPayServer.Configuration
BundleJsCss = conf.GetOrDefault<bool>("bundlejscss", true);
DockerDeployment = conf.GetOrDefault<bool>("dockerdeployment", true);
AllowAdminRegistration = conf.GetOrDefault<bool>("allow-admin-registration", false);
TorrcFile = conf.GetOrDefault<string>("torrcfile", null);
TorServices = conf.GetOrDefault<string>("torservices", null)
?.Split(new[] {';', ','}, StringSplitOptions.RemoveEmptyEntries);
if (!string.IsNullOrEmpty(TorrcFile) && TorServices != null)
throw new ConfigException($"torrcfile or torservices should be provided, but not both");
var socksEndpointString = conf.GetOrDefault<string>("socksendpoint", null);
if (!string.IsNullOrEmpty(socksEndpointString))
@ -200,7 +195,6 @@ namespace BTCPayServer.Configuration
set;
}
public string TorrcFile { get; set; }
public string[] TorServices { get; set; }
public Uri UpdateUrl { get; set; }
}
}

@ -39,7 +39,6 @@ namespace BTCPayServer.Configuration
app.Option("--sshauthorizedkeys", "Path to a authorized_keys file that BTCPayServer can modify from the website (default: empty)", CommandOptionType.SingleValue);
app.Option("--sshtrustedfingerprints", "SSH Host public key fingerprint or sha256 (default: empty, it will allow untrusted connections)", CommandOptionType.SingleValue);
app.Option("--torrcfile", "Path to torrc file containing hidden services directories (default: empty)", CommandOptionType.SingleValue);
app.Option("--torservices", "Tor hostnames of available services added to Server Settings (and sets onion header for btcpay). Format: btcpayserver:host.onion:80;btc-p2p:host2.onion:81,BTC-RPC:host3.onion:82,UNKNOWN:host4.onion:83. (default: empty)", CommandOptionType.SingleValue);
app.Option("--socksendpoint", "Socks endpoint to connect to onion urls (default: empty)", CommandOptionType.SingleValue);
app.Option("--updateurl", $"Url used for once a day new release version check. Check performed only if value is not empty (default: empty)", CommandOptionType.SingleValue);
app.Option("--debuglog", "A rolling log file for debug messages.", CommandOptionType.SingleValue);

@ -1,24 +1,26 @@
using System;
using System.Globalization;
using System.Security.Policy;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.Fido2;
using BTCPayServer.Fido2.Models;
using BTCPayServer.Logging;
using BTCPayServer.Models;
using BTCPayServer.Models.AccountViewModels;
using BTCPayServer.Security;
using BTCPayServer.Services;
using Fido2NetLib;
using BTCPayServer.U2F;
using BTCPayServer.U2F.Models;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Linq;
using NicolasDorier.RateLimits;
using U2F.Core.Exceptions;
namespace BTCPayServer.Controllers
{
@ -32,7 +34,8 @@ namespace BTCPayServer.Controllers
readonly SettingsRepository _SettingsRepository;
readonly Configuration.BTCPayServerOptions _Options;
private readonly BTCPayServerEnvironment _btcPayServerEnvironment;
private readonly Fido2Service _fido2Service;
public U2FService _u2FService;
private readonly RateLimitService _rateLimitService;
private readonly EventAggregator _eventAggregator;
readonly ILogger _logger;
@ -43,8 +46,9 @@ namespace BTCPayServer.Controllers
SettingsRepository settingsRepository,
Configuration.BTCPayServerOptions options,
BTCPayServerEnvironment btcPayServerEnvironment,
EventAggregator eventAggregator,
Fido2Service fido2Service)
U2FService u2FService,
RateLimitService rateLimitService,
EventAggregator eventAggregator)
{
_userManager = userManager;
_signInManager = signInManager;
@ -52,7 +56,8 @@ namespace BTCPayServer.Controllers
_SettingsRepository = settingsRepository;
_Options = options;
_btcPayServerEnvironment = btcPayServerEnvironment;
_fido2Service = fido2Service;
_u2FService = u2FService;
_rateLimitService = rateLimitService;
_eventAggregator = eventAggregator;
_logger = Logs.PayServer;
}
@ -120,8 +125,7 @@ namespace BTCPayServer.Controllers
return View(model);
}
var fido2Devices = await _fido2Service.HasCredentials(user.Id);
if (!await _userManager.IsLockedOutAsync(user) && fido2Devices)
if (!await _userManager.IsLockedOutAsync(user) && await _u2FService.HasDevices(user.Id))
{
if (await _userManager.CheckPasswordAsync(user, model.Password))
{
@ -140,7 +144,7 @@ namespace BTCPayServer.Controllers
return View("SecondaryLogin", new SecondaryLoginViewModel()
{
LoginWith2FaViewModel = twoFModel,
LoginWithFido2ViewModel = fido2Devices? await BuildFido2ViewModel(model.RememberMe, user): null,
LoginWithU2FViewModel = await BuildU2FViewModel(model.RememberMe, user)
});
}
else
@ -156,7 +160,7 @@ namespace BTCPayServer.Controllers
var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: true);
if (result.Succeeded)
{
_logger.LogInformation($"User '{user.Id}' logged in.");
_logger.LogInformation("User logged in.");
return RedirectToLocal(returnUrl);
}
if (result.RequiresTwoFactor)
@ -171,7 +175,7 @@ namespace BTCPayServer.Controllers
}
if (result.IsLockedOut)
{
_logger.LogWarning($"User '{user.Id}' account locked out.");
_logger.LogWarning("User account locked out.");
return RedirectToAction(nameof(Lockout));
}
else
@ -185,29 +189,31 @@ namespace BTCPayServer.Controllers
return View(model);
}
private async Task<LoginWithFido2ViewModel> BuildFido2ViewModel(bool rememberMe, ApplicationUser user)
private async Task<LoginWithU2FViewModel> BuildU2FViewModel(bool rememberMe, ApplicationUser user)
{
if (_btcPayServerEnvironment.IsSecure)
{
var r = await _fido2Service.RequestLogin(user.Id);
if (r is null)
var u2fChallenge = await _u2FService.GenerateDeviceChallenges(user.Id,
Request.GetAbsoluteUriNoPathBase().ToString().TrimEnd('/'));
return new LoginWithU2FViewModel()
{
return null;
}
return new LoginWithFido2ViewModel()
{
Data = r,
Version = u2fChallenge[0].version,
Challenge = u2fChallenge[0].challenge,
Challenges = u2fChallenge,
AppId = u2fChallenge[0].appId,
UserId = user.Id,
RememberMe = rememberMe
};
}
return null;
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> LoginWithFido2(LoginWithFido2ViewModel viewModel, string returnUrl = null)
public async Task<IActionResult> LoginWithU2F(LoginWithU2FViewModel viewModel, string returnUrl = null)
{
if (!CanLoginOrRegister())
{
@ -225,25 +231,24 @@ namespace BTCPayServer.Controllers
var errorMessage = string.Empty;
try
{
if (await _fido2Service.CompleteLogin(viewModel.UserId, JObject.Parse(viewModel.Response).ToObject<AuthenticatorAssertionRawResponse>()))
if (await _u2FService.AuthenticateUser(viewModel.UserId, viewModel.DeviceResponse))
{
await _signInManager.SignInAsync(user, viewModel.RememberMe, "FIDO2");
await _signInManager.SignInAsync(user, viewModel.RememberMe, "U2F");
_logger.LogInformation("User logged in.");
return RedirectToLocal(returnUrl);
}
errorMessage = "Invalid login attempt.";
}
catch (Fido2VerificationException e)
catch (U2fException e)
{
errorMessage = e.Message;
}
ModelState.AddModelError(string.Empty, errorMessage);
viewModel.Response = null;
return View("SecondaryLogin", new SecondaryLoginViewModel()
{
LoginWithFido2ViewModel = viewModel,
LoginWithU2FViewModel = viewModel,
LoginWith2FaViewModel = !user.TwoFactorEnabled
? null
: new LoginWith2faViewModel()
@ -252,6 +257,7 @@ namespace BTCPayServer.Controllers
}
});
}
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> LoginWith2fa(bool rememberMe, string returnUrl = null)
@ -274,7 +280,7 @@ namespace BTCPayServer.Controllers
return View("SecondaryLogin", new SecondaryLoginViewModel()
{
LoginWith2FaViewModel = new LoginWith2faViewModel { RememberMe = rememberMe },
LoginWithFido2ViewModel = (await _fido2Service.HasCredentials(user.Id)) ? await BuildFido2ViewModel(rememberMe, user) : null,
LoginWithU2FViewModel = (await _u2FService.HasDevices(user.Id)) ? await BuildU2FViewModel(rememberMe, user) : null
});
}
@ -320,7 +326,7 @@ namespace BTCPayServer.Controllers
return View("SecondaryLogin", new SecondaryLoginViewModel()
{
LoginWith2FaViewModel = model,
LoginWithFido2ViewModel = (await _fido2Service.HasCredentials(user.Id)) ? await BuildFido2ViewModel(rememberMe, user) : null,
LoginWithU2FViewModel = (await _u2FService.HasDevices(user.Id)) ? await BuildU2FViewModel(rememberMe, user) : null
});
}
}

@ -16,22 +16,23 @@ namespace BTCPayServer.Controllers
public string StoreId { get; set; }
public override string ToString()
{
return string.Empty;
return String.Empty;
}
}
[HttpGet("{appId}/settings/crowdfund")]
[HttpGet]
[Route("{appId}/settings/crowdfund")]
public async Task<IActionResult> UpdateCrowdfund(string appId)
{
var app = await GetOwnedApp(appId, AppType.Crowdfund);
if (app == null)
return NotFound();
var settings = app.GetSettings<CrowdfundSettings>();
var vm = new UpdateCrowdfundViewModel
var vm = new UpdateCrowdfundViewModel()
{
Title = settings.Title,
StoreId = app.StoreDataId,
StoreName = app.StoreData?.StoreName,
Enabled = settings.Enabled,
EnforceTargetAmount = settings.EnforceTargetAmount,
StartDate = settings.StartDate,

@ -97,11 +97,10 @@ namespace BTCPayServer.Controllers
settings.DefaultView = settings.EnableShoppingCart ? PosViewType.Cart : settings.DefaultView;
settings.EnableShoppingCart = false;
var vm = new UpdatePointOfSaleViewModel
var vm = new UpdatePointOfSaleViewModel()
{
Id = appId,
StoreId = app.StoreDataId,
StoreName = app.StoreData?.StoreName,
Title = settings.Title,
DefaultView = settings.DefaultView,
ShowCustomAmount = settings.ShowCustomAmount,
@ -184,7 +183,7 @@ namespace BTCPayServer.Controllers
var app = await GetOwnedApp(appId, AppType.PointOfSale);
if (app == null)
return NotFound();
app.SetSettings(new PointOfSaleSettings
app.SetSettings(new PointOfSaleSettings()
{
Title = vm.Title,
DefaultView = vm.DefaultView,

@ -8,11 +8,6 @@ namespace BTCPayServer.Controllers.GreenField
public static class GreenFieldUtils
{
public static IActionResult CreateValidationError(this ControllerBase controller, ModelStateDictionary modelState)
{
return controller.UnprocessableEntity(modelState.ToGreenfieldValidationError());
}
public static List<GreenfieldValidationError> ToGreenfieldValidationError(this ModelStateDictionary modelState)
{
List<GreenfieldValidationError> errors = new List<GreenfieldValidationError>();
foreach (var error in modelState)
@ -22,17 +17,11 @@ namespace BTCPayServer.Controllers.GreenField
errors.Add(new GreenfieldValidationError(error.Key, errorMessage.ErrorMessage));
}
}
return errors;
return controller.UnprocessableEntity(errors.ToArray());
}
public static IActionResult CreateAPIError(this ControllerBase controller, string errorCode, string errorMessage)
{
return controller.BadRequest(new GreenfieldAPIError(errorCode, errorMessage));
}
public static IActionResult CreateAPIError(this ControllerBase controller, int httpCode, string errorCode, string errorMessage)
{
return controller.StatusCode(httpCode, new GreenfieldAPIError(errorCode, errorMessage));
}
}
}

@ -1,7 +1,6 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using System.Globalization;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
@ -48,43 +47,25 @@ namespace BTCPayServer.Controllers.GreenField
[Authorize(Policy = Policies.CanViewInvoices,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}/invoices")]
public async Task<IActionResult> GetInvoices(string storeId, [FromQuery] string[] orderId = null, [FromQuery] string[] status = null,
[FromQuery]
[ModelBinder(typeof(ModelBinders.DateTimeOffsetModelBinder))]
DateTimeOffset? startDate = null,
[FromQuery]
[ModelBinder(typeof(ModelBinders.DateTimeOffsetModelBinder))]
DateTimeOffset? endDate = null, [FromQuery] bool includeArchived = false)
public async Task<IActionResult> GetInvoices(string storeId, bool includeArchived = false)
{
var store = HttpContext.GetStoreData();
if (store == null)
{
return StoreNotFound();
}
if (startDate is DateTimeOffset s &&
endDate is DateTimeOffset e &&
s > e)
{
this.ModelState.AddModelError(nameof(startDate), "startDate should not be above endDate");
this.ModelState.AddModelError(nameof(endDate), "endDate should not be below startDate");
return NotFound();
}
if (!ModelState.IsValid)
return this.CreateValidationError(ModelState);
var invoices =
await _invoiceRepository.GetInvoices(new InvoiceQuery()
{
StoreId = new[] {store.Id},
IncludeArchived = includeArchived,
StartDate = startDate,
EndDate = endDate,
OrderId = orderId,
Status = status
StoreId = new[] { store.Id },
IncludeArchived = includeArchived
});
return Ok(invoices.Select(ToModel));
}
[Authorize(Policy = Policies.CanViewInvoices,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}/invoices/{invoiceId}")]
@ -93,13 +74,13 @@ namespace BTCPayServer.Controllers.GreenField
var store = HttpContext.GetStoreData();
if (store == null)
{
return InvoiceNotFound();
return NotFound();
}
var invoice = await _invoiceRepository.GetInvoice(invoiceId, true);
if (invoice?.StoreId != store.Id)
{
return InvoiceNotFound();
return NotFound();
}
return Ok(ToModel(invoice));
@ -113,13 +94,9 @@ namespace BTCPayServer.Controllers.GreenField
var store = HttpContext.GetStoreData();
if (store == null)
{
return InvoiceNotFound();
}
var invoice = await _invoiceRepository.GetInvoice(invoiceId, true);
if (invoice?.StoreId != store.Id)
{
return InvoiceNotFound();
return NotFound();
}
await _invoiceRepository.ToggleInvoiceArchival(invoiceId, true, storeId);
return Ok();
}
@ -132,7 +109,7 @@ namespace BTCPayServer.Controllers.GreenField
var store = HttpContext.GetStoreData();
if (store == null)
{
return InvoiceNotFound();
return NotFound();
}
var result = await _invoiceRepository.UpdateInvoiceMetadata(invoiceId, storeId, request.Metadata);
@ -141,7 +118,7 @@ namespace BTCPayServer.Controllers.GreenField
return Ok(ToModel(result));
}
return InvoiceNotFound();
return NotFound();
}
[Authorize(Policy = Policies.CanCreateInvoice,
@ -152,7 +129,7 @@ namespace BTCPayServer.Controllers.GreenField
var store = HttpContext.GetStoreData();
if (store == null)
{
return StoreNotFound();
return NotFound();
}
if (request.Amount < 0.0m)
@ -229,13 +206,13 @@ namespace BTCPayServer.Controllers.GreenField
var store = HttpContext.GetStoreData();
if (store == null)
{
return InvoiceNotFound();
return NotFound();
}
var invoice = await _invoiceRepository.GetInvoice(invoiceId, true);
if (invoice.StoreId != store.Id)
{
return InvoiceNotFound();
return NotFound();
}
if (!await _invoiceRepository.MarkInvoiceStatus(invoice.Id, request.Status))
@ -258,13 +235,13 @@ namespace BTCPayServer.Controllers.GreenField
var store = HttpContext.GetStoreData();
if (store == null)
{
return InvoiceNotFound();
return NotFound();
}
var invoice = await _invoiceRepository.GetInvoice(invoiceId, true);
if (invoice.StoreId != store.Id)
{
return InvoiceNotFound();
return NotFound();
}
if (!invoice.Archived)
@ -283,21 +260,21 @@ namespace BTCPayServer.Controllers.GreenField
[Authorize(Policy = Policies.CanViewInvoices,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}/invoices/{invoiceId}/payment-methods")]
public async Task<IActionResult> GetInvoicePaymentMethods(string storeId, string invoiceId, bool onlyAccountedPayments = true)
public async Task<IActionResult> GetInvoicePaymentMethods(string storeId, string invoiceId)
{
var store = HttpContext.GetStoreData();
if (store == null)
{
return InvoiceNotFound();
return NotFound();
}
var invoice = await _invoiceRepository.GetInvoice(invoiceId, true);
if (invoice?.StoreId != store.Id)
{
return InvoiceNotFound();
return NotFound();
}
return Ok(ToPaymentMethodModels(invoice, onlyAccountedPayments));
return Ok(ToPaymentMethodModels(invoice));
}
[Authorize(Policy = Policies.CanViewInvoices,
@ -308,13 +285,13 @@ namespace BTCPayServer.Controllers.GreenField
var store = HttpContext.GetStoreData();
if (store == null)
{
return InvoiceNotFound();
return NotFound();
}
var invoice = await _invoiceRepository.GetInvoice(invoiceId, true);
if (invoice?.StoreId != store.Id)
{
return InvoiceNotFound();
return NotFound();
}
if (PaymentMethodId.TryParse(paymentMethod, out var paymentMethodId))
@ -323,27 +300,17 @@ namespace BTCPayServer.Controllers.GreenField
_paymentMethodHandlerDictionary, store, invoice, paymentMethodId);
return Ok();
}
ModelState.AddModelError(nameof(paymentMethod), "Invalid payment method");
return this.CreateValidationError(ModelState);
return BadRequest();
}
private IActionResult InvoiceNotFound()
{
return this.CreateAPIError(404, "invoice-not-found", "The invoice was not found");
}
private IActionResult StoreNotFound()
{
return this.CreateAPIError(404, "store-not-found", "The store was not found");
}
private InvoicePaymentMethodDataModel[] ToPaymentMethodModels(InvoiceEntity entity, bool includeAccountedPaymentOnly)
private InvoicePaymentMethodDataModel[] ToPaymentMethodModels(InvoiceEntity entity)
{
return entity.GetPaymentMethods().Select(
method =>
{
var accounting = method.Calculate();
var details = method.GetPaymentMethodDetails();
var payments = method.ParentEntity.GetPayments(includeAccountedPaymentOnly).Where(paymentEntity =>
var payments = method.ParentEntity.GetPayments().Where(paymentEntity =>
paymentEntity.GetPaymentMethodId() == method.GetId());
return new InvoicePaymentMethodDataModel()

@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@ -32,15 +31,13 @@ namespace BTCPayServer.Controllers.GreenField
private readonly CurrencyNameTable _currencyNameTable;
private readonly BTCPayNetworkJsonSerializerSettings _serializerSettings;
private readonly BTCPayNetworkProvider _networkProvider;
private readonly IEnumerable<IPayoutHandler> _payoutHandlers;
public GreenfieldPullPaymentController(PullPaymentHostedService pullPaymentService,
LinkGenerator linkGenerator,
ApplicationDbContextFactory dbContextFactory,
CurrencyNameTable currencyNameTable,
Services.BTCPayNetworkJsonSerializerSettings serializerSettings,
BTCPayNetworkProvider networkProvider,
IEnumerable<IPayoutHandler> payoutHandlers)
BTCPayNetworkProvider networkProvider)
{
_pullPaymentService = pullPaymentService;
_linkGenerator = linkGenerator;
@ -48,7 +45,6 @@ namespace BTCPayServer.Controllers.GreenField
_currencyNameTable = currencyNameTable;
_serializerSettings = serializerSettings;
_networkProvider = networkProvider;
_payoutHandlers = payoutHandlers;
}
[HttpGet("~/api/v1/stores/{storeId}/pull-payments")]
@ -182,7 +178,7 @@ namespace BTCPayServer.Controllers.GreenField
return NotFound();
using var ctx = _dbContextFactory.CreateContext();
var payouts = await ctx.Payouts.Where(p => p.PullPaymentDataId == pullPaymentId)
.Where(p => p.State != PayoutState.Cancelled || includeCancelled)
.Where(p => p.State != Data.PayoutState.Cancelled || includeCancelled)
.ToListAsync();
var cd = _currencyNameTable.GetCurrencyData(pp.GetBlob().Currency, false);
return base.Ok(payouts
@ -200,9 +196,14 @@ namespace BTCPayServer.Controllers.GreenField
Amount = blob.Amount,
PaymentMethodAmount = blob.CryptoAmount,
Revision = blob.Revision,
State = p.State
State = p.State == Data.PayoutState.AwaitingPayment ? Client.Models.PayoutState.AwaitingPayment :
p.State == Data.PayoutState.AwaitingApproval ? Client.Models.PayoutState.AwaitingApproval :
p.State == Data.PayoutState.Cancelled ? Client.Models.PayoutState.Cancelled :
p.State == Data.PayoutState.Completed ? Client.Models.PayoutState.Completed :
p.State == Data.PayoutState.InProgress ? Client.Models.PayoutState.InProgress :
throw new NotSupportedException(),
};
model.Destination = blob.Destination;
model.Destination = blob.Destination.ToString();
model.PaymentMethod = p.PaymentMethodId;
return model;
}
@ -213,14 +214,10 @@ namespace BTCPayServer.Controllers.GreenField
{
if (request is null)
return NotFound();
if (!PaymentMethodId.TryParse(request?.PaymentMethod, out var paymentMethodId))
{
ModelState.AddModelError(nameof(request.PaymentMethod), "Invalid payment method");
return this.CreateValidationError(ModelState);
}
var payoutHandler = _payoutHandlers.FirstOrDefault(handler => handler.CanHandle(paymentMethodId));
if (payoutHandler is null)
var network = request?.PaymentMethod is string paymentMethod ?
this._networkProvider.GetNetwork<BTCPayNetwork>(paymentMethod) : null;
if (network is null)
{
ModelState.AddModelError(nameof(request.PaymentMethod), "Invalid payment method");
return this.CreateValidationError(ModelState);
@ -231,8 +228,7 @@ namespace BTCPayServer.Controllers.GreenField
if (pp is null)
return NotFound();
var ppBlob = pp.GetBlob();
IClaimDestination destination = await payoutHandler.ParseClaimDestination(paymentMethodId,request.Destination);
if (destination is null)
if (request.Destination is null || !ClaimDestination.TryParse(request.Destination, network, out var destination))
{
ModelState.AddModelError(nameof(request.Destination), "The destination must be an address or a BIP21 URI");
return this.CreateValidationError(ModelState);
@ -249,7 +245,7 @@ namespace BTCPayServer.Controllers.GreenField
Destination = destination,
PullPaymentId = pullPaymentId,
Value = request.Amount,
PaymentMethodId = paymentMethodId
PaymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike)
});
switch (result.Result)
{

@ -239,7 +239,6 @@ namespace BTCPayServer.Controllers.GreenField
coin.OutPoint.Hash.ToString()),
Timestamp = coin.Timestamp,
KeyPath = coin.KeyPath,
Confirmations = coin.Confirmations,
Address = network.NBXplorerNetwork.CreateAddress(derivationScheme.AccountDerivation, coin.KeyPath, coin.ScriptPubKey).ToString()
};
}).ToList()

@ -48,7 +48,7 @@ namespace BTCPayServer.Controllers.GreenField
{
var w = await StoreRepository.GetWebhook(CurrentStoreId, webhookId);
if (w is null)
return WebhookNotFound();
return NotFound();
return Ok(FromModel(w, false));
}
}
@ -70,7 +70,7 @@ namespace BTCPayServer.Controllers.GreenField
var webhookId = await StoreRepository.CreateWebhook(CurrentStoreId, ToModel(create));
var w = await StoreRepository.GetWebhook(CurrentStoreId, webhookId);
if (w is null)
return WebhookNotFound();
return NotFound();
return Ok(FromModel(w, true));
}
@ -88,7 +88,7 @@ namespace BTCPayServer.Controllers.GreenField
return this.CreateValidationError(ModelState);
var w = await StoreRepository.GetWebhook(CurrentStoreId, webhookId);
if (w is null)
return WebhookNotFound();
return NotFound();
await StoreRepository.UpdateWebhook(storeId, webhookId, ToModel(update));
return await ListWebhooks(webhookId);
}
@ -97,19 +97,10 @@ namespace BTCPayServer.Controllers.GreenField
{
var w = await StoreRepository.GetWebhook(CurrentStoreId, webhookId);
if (w is null)
return WebhookNotFound();
return NotFound();
await StoreRepository.DeleteWebhook(CurrentStoreId, webhookId);
return Ok();
}
IActionResult WebhookNotFound()
{
return this.CreateAPIError(404, "webhook-not-found", "The webhook was not found");
}
IActionResult WebhookDeliveryNotFound()
{
return this.CreateAPIError(404, "webhookdelivery-not-found", "The webhook delivery was not found");
}
private WebhookBlob ToModel(StoreWebhookBaseData create)
{
return new WebhookBlob()
@ -142,7 +133,7 @@ namespace BTCPayServer.Controllers.GreenField
{
var delivery = await StoreRepository.GetWebhookDelivery(CurrentStoreId, webhookId, deliveryId);
if (delivery is null)
return WebhookDeliveryNotFound();
return NotFound();
return Ok(FromModel(delivery));
}
}
@ -151,7 +142,7 @@ namespace BTCPayServer.Controllers.GreenField
{
var delivery = await StoreRepository.GetWebhookDelivery(CurrentStoreId, webhookId, deliveryId);
if (delivery is null)
return WebhookDeliveryNotFound();
return NotFound();
return this.Ok(new JValue(await WebhookNotificationManager.Redeliver(deliveryId)));
}
@ -160,7 +151,7 @@ namespace BTCPayServer.Controllers.GreenField
{
var delivery = await StoreRepository.GetWebhookDelivery(CurrentStoreId, webhookId, deliveryId);
if (delivery is null)
return WebhookDeliveryNotFound();
return NotFound();
return File(delivery.GetBlob().Request, "application/json");
}

@ -64,7 +64,7 @@ namespace BTCPayServer.Controllers
}
[Route("misc/lang")]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie + "," + AuthenticationSchemes.Greenfield)]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public IActionResult Languages()
{
return Json(LanguageService.GetLanguages(), new JsonSerializerSettings() { Formatting = Formatting.Indented });

@ -19,8 +19,8 @@ using BTCPayServer.HostedServices;
using BTCPayServer.Models;
using BTCPayServer.Models.InvoicingModels;
using BTCPayServer.Payments;
using BTCPayServer.Payments.CoinSwitch;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Plugins.CoinSwitch;
using BTCPayServer.Rating;
using BTCPayServer.Security;
using BTCPayServer.Services.Invoices;
@ -359,7 +359,7 @@ namespace BTCPayServer.Controllers
return new InvoiceDetailsModel
{
Archived = invoice.Archived,
Payments = invoice.GetPayments(false),
Payments = invoice.GetPayments(),
CryptoPayments = invoice.GetPaymentMethods().Select(
data =>
{
@ -525,6 +525,12 @@ namespace BTCPayServer.Controllers
var storeBlob = store.GetStoreBlob();
var accounting = paymentMethod.Calculate();
CoinSwitchSettings coinswitch = (storeBlob.CoinSwitchSettings != null && storeBlob.CoinSwitchSettings.Enabled &&
storeBlob.CoinSwitchSettings.IsConfigured())
? storeBlob.CoinSwitchSettings
: null;
var paymentMethodHandler = _paymentMethodHandlerDictionary[paymentMethodId];
var divisibility = _CurrencyNameTable.GetNumberFormatInfo(paymentMethod.GetId().CryptoCode, false)?.CurrencyDecimalDigits;
@ -561,7 +567,11 @@ namespace BTCPayServer.Controllers
Status = invoice.StatusString,
#pragma warning restore CS0618 // Type or member is obsolete
NetworkFee = paymentMethodDetails.GetNextNetworkFee(),
IsMultiCurrency = invoice.GetPayments(false).Select(p => p.GetPaymentMethodId()).Concat(new[] { paymentMethod.GetId() }).Distinct().Count() > 1,
IsMultiCurrency = invoice.GetPayments().Select(p => p.GetPaymentMethodId()).Concat(new[] { paymentMethod.GetId() }).Distinct().Count() > 1,
CoinSwitchEnabled = coinswitch != null,
CoinSwitchAmountMarkupPercentage = coinswitch?.AmountMarkupPercentage ?? 0,
CoinSwitchMerchantId = coinswitch?.MerchantId,
CoinSwitchMode = coinswitch?.Mode,
StoreId = store.Id,
AvailableCryptos = invoice.GetPaymentMethods()
.Where(i => i.Network != null)
@ -903,20 +913,21 @@ namespace BTCPayServer.Controllers
var jObject = JObject.Parse(posData);
foreach (var item in jObject)
{
switch (item.Value.Type)
{
case JTokenType.Array:
var items = item.Value.AsEnumerable().ToList();
for (var i = 0; i < items.Count; i++)
{
result.TryAdd($"{item.Key}[{i}]", ParsePosData(items[i].ToString()));
result.Add($"{item.Key}[{i}]", ParsePosData(items[i].ToString()));
}
break;
case JTokenType.Object:
result.TryAdd(item.Key, ParsePosData(item.Value.ToString()));
result.Add(item.Key, ParsePosData(item.Value.ToString()));
break;
default:
result.TryAdd(item.Key, item.Value.ToString());
result.Add(item.Key, item.Value.ToString());
break;
}
@ -924,7 +935,7 @@ namespace BTCPayServer.Controllers
}
catch
{
result.TryAdd(string.Empty, posData);
result.Add(string.Empty, posData);
}
return result;
}

@ -472,8 +472,8 @@ namespace BTCPayServer.Controllers
{BTCPayServer.Client.Policies.CanCreateUser, ("Create new users", "The app will be able to create new users on this server.")},
{BTCPayServer.Client.Policies.CanModifyStoreSettings, ("Modify your stores", "The app will be able to view, modify, delete and create new invoices on all your stores.")},
{$"{BTCPayServer.Client.Policies.CanModifyStoreSettings}:", ("Manage selected stores", "The app will be able to view, modify, delete and create new invoices on the selected stores.")},
{BTCPayServer.Client.Policies.CanModifyStoreWebhooks, ("Modify stores webhooks", "The app will modify the webhooks of all your stores.")},
{$"{BTCPayServer.Client.Policies.CanModifyStoreWebhooks}:", ("Modify selected stores' webhooks", "The app will modify the webhooks of the selected stores.")},
{BTCPayServer.Client.Policies.CanModifyStoreWebhooks, ("Modify stores webhooks", "The app will be mofidy the webhooks of all your stores.")},
{$"{BTCPayServer.Client.Policies.CanModifyStoreWebhooks}:", ("Modify selected stores' webhooks", "The app will be mofidy the webhooks of the selected stores.")},
{BTCPayServer.Client.Policies.CanViewStoreSettings, ("View your stores", "The app will be able to view stores settings.")},
{$"{BTCPayServer.Client.Policies.CanViewStoreSettings}:", ("View your stores", "The app will be able to view the selected stores' settings.")},
{BTCPayServer.Client.Policies.CanModifyServerSettings, ("Manage your server", "The app will have total control on the server settings of your server")},

@ -0,0 +1,83 @@
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Models;
using BTCPayServer.U2F.Models;
using Microsoft.AspNetCore.Mvc;
using U2F.Core.Exceptions;
namespace BTCPayServer.Controllers
{
public partial class ManageController
{
[HttpGet]
public async Task<IActionResult> U2FAuthentication()
{
return View(new U2FAuthenticationViewModel()
{
Devices = await _u2FService.GetDevices(_userManager.GetUserId(User))
});
}
[HttpGet]
public async Task<IActionResult> RemoveU2FDevice(string id)
{
await _u2FService.RemoveDevice(id, _userManager.GetUserId(User));
return RedirectToAction("U2FAuthentication", new
{
StatusMessage = "Device removed"
});
}
[HttpGet]
public IActionResult AddU2FDevice(string name)
{
if (!_btcPayServerEnvironment.IsSecure)
{
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Error,
Message = "Cannot register U2F device while not on https or tor"
});
return RedirectToAction("U2FAuthentication");
}
var serverRegisterResponse = _u2FService.StartDeviceRegistration(_userManager.GetUserId(User),
Request.GetAbsoluteUriNoPathBase().ToString().TrimEnd('/'));
return View(new AddU2FDeviceViewModel()
{
AppId = serverRegisterResponse.AppId,
Challenge = serverRegisterResponse.Challenge,
Version = serverRegisterResponse.Version,
Name = name
});
}
[HttpPost]
public async Task<IActionResult> AddU2FDevice(AddU2FDeviceViewModel viewModel)
{
var errorMessage = string.Empty;
try
{
if (await _u2FService.CompleteRegistration(_userManager.GetUserId(User), viewModel.DeviceResponse,
string.IsNullOrEmpty(viewModel.Name) ? "Unlabelled U2F Device" : viewModel.Name))
{
TempData[WellKnownTempData.SuccessMessage] = "Device added!";
return RedirectToAction("U2FAuthentication");
}
}
catch (U2fException e)
{
errorMessage = e.Message;
}
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Error,
Message = string.IsNullOrEmpty(errorMessage) ? "Could not add device." : errorMessage
});
return RedirectToAction("U2FAuthentication");
}
}
}

@ -4,11 +4,13 @@ using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Data;
using BTCPayServer.Models.ManageViewModels;
using BTCPayServer.Security;
using BTCPayServer.Security.GreenField;
using BTCPayServer.Services;
using BTCPayServer.Services.Mails;
using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Wallets;
using BTCPayServer.U2F;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
@ -27,6 +29,8 @@ namespace BTCPayServer.Controllers
private readonly EmailSenderFactory _EmailSenderFactory;
private readonly ILogger _logger;
private readonly UrlEncoder _urlEncoder;
readonly IWebHostEnvironment _Env;
public U2FService _u2FService;
private readonly BTCPayServerEnvironment _btcPayServerEnvironment;
private readonly APIKeyRepository _apiKeyRepository;
private readonly IAuthorizationService _authorizationService;
@ -44,6 +48,7 @@ namespace BTCPayServer.Controllers
BTCPayWalletProvider walletProvider,
StoreRepository storeRepository,
IWebHostEnvironment env,
U2FService u2FService,
BTCPayServerEnvironment btcPayServerEnvironment,
APIKeyRepository apiKeyRepository,
IAuthorizationService authorizationService,
@ -55,6 +60,8 @@ namespace BTCPayServer.Controllers
_EmailSenderFactory = emailSenderFactory;
_logger = logger;
_urlEncoder = urlEncoder;
_Env = env;
_u2FService = u2FService;
_btcPayServerEnvironment = btcPayServerEnvironment;
_apiKeyRepository = apiKeyRepository;
_authorizationService = authorizationService;

@ -1,11 +1,11 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using BTCPayServer;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.HostedServices;
using BTCPayServer.Models;
@ -26,21 +26,18 @@ namespace BTCPayServer.Controllers
private readonly CurrencyNameTable _currencyNameTable;
private readonly PullPaymentHostedService _pullPaymentHostedService;
private readonly BTCPayNetworkJsonSerializerSettings _serializerSettings;
private readonly IEnumerable<IPayoutHandler> _payoutHandlers;
public PullPaymentController(ApplicationDbContextFactory dbContextFactory,
BTCPayNetworkProvider networkProvider,
CurrencyNameTable currencyNameTable,
PullPaymentHostedService pullPaymentHostedService,
BTCPayNetworkJsonSerializerSettings serializerSettings,
IEnumerable<IPayoutHandler> payoutHandlers)
BTCPayServer.Services.BTCPayNetworkJsonSerializerSettings serializerSettings)
{
_dbContextFactory = dbContextFactory;
_networkProvider = networkProvider;
_currencyNameTable = currencyNameTable;
_pullPaymentHostedService = pullPaymentHostedService;
_serializerSettings = serializerSettings;
_payoutHandlers = payoutHandlers;
}
[Route("pull-payments/{pullPaymentId}")]
public async Task<IActionResult> ViewPullPayment(string pullPaymentId)
@ -58,7 +55,7 @@ namespace BTCPayServer.Controllers
{
Entity = o,
Blob = o.GetBlob(_serializerSettings),
ProofBlob = _payoutHandlers.FirstOrDefault(handler => handler.CanHandle(o.GetPaymentMethodId()))?.ParseProof(o)
TransactionId = o.GetProofBlob(_serializerSettings)?.TransactionId?.ToString()
});
var cd = _currencyNameTable.GetCurrencyData(blob.Currency, false);
var totalPaid = payouts.Where(p => p.Entity.State != PayoutState.Cancelled).Select(p => p.Blob.Amount).Sum();
@ -82,10 +79,10 @@ namespace BTCPayServer.Controllers
Amount = entity.Blob.Amount,
AmountFormatted = _currencyNameTable.FormatCurrency(entity.Blob.Amount, blob.Currency),
Currency = blob.Currency,
Status = entity.Entity.State,
Destination = entity.Blob.Destination,
Link = entity.ProofBlob?.Link,
TransactionId = entity.ProofBlob?.Id
Status = entity.Entity.State.GetStateString(),
Destination = entity.Blob.Destination.Address.ToString(),
Link = GetTransactionLink(_networkProvider.GetNetwork<BTCPayNetwork>(entity.Entity.GetPaymentMethodId().CryptoCode), entity.TransactionId),
TransactionId = entity.TransactionId
}).ToList()
};
vm.IsPending &= vm.AmountDue > 0.0m;
@ -102,14 +99,11 @@ namespace BTCPayServer.Controllers
{
ModelState.AddModelError(nameof(pullPaymentId), "This pull payment does not exists");
}
var ppBlob = pp.GetBlob();
var network = _networkProvider.GetNetwork<BTCPayNetwork>(ppBlob.SupportedPaymentMethods.Single().CryptoCode);
var paymentMethodId = ppBlob.SupportedPaymentMethods.Single();
var payoutHandler = _payoutHandlers.FirstOrDefault(handler => handler.CanHandle(paymentMethodId));
IClaimDestination destination = await payoutHandler?.ParseClaimDestination(paymentMethodId, vm.Destination);
if (destination is null)
IClaimDestination destination = null;
if (network != null &&
(!ClaimDestination.TryParse(vm.Destination, network, out destination) || destination is null))
{
ModelState.AddModelError(nameof(vm.Destination), $"Invalid destination");
}

@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Extensions;
@ -20,8 +19,6 @@ using BTCPayServer.Storage.ViewModels;
using BTCPayServer.Views;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Controllers
@ -33,14 +30,13 @@ namespace BTCPayServer.Controllers
{
var fileUrl = string.IsNullOrEmpty(fileId) ? null : await _FileService.GetFileUrl(Request.GetAbsoluteRootUri(), fileId);
var model = new ViewFilesViewModel()
return View(new ViewFilesViewModel()
{
Files = await _StoredFileRepository.GetFiles(),
SelectedFileId = string.IsNullOrEmpty(fileUrl) ? null : fileId,
DirectFileUrl = fileUrl,
StorageConfigured = (await _SettingsRepository.GetSettingAsync<StorageSettings>()) != null
};
return View(model);
});
}
[HttpGet("server/files/{fileId}/delete")]
@ -178,13 +174,8 @@ namespace BTCPayServer.Controllers
var savedSettings = await _SettingsRepository.GetSettingAsync<StorageSettings>();
if (forceChoice || savedSettings == null)
{
var providersList = _StorageProviderServices.Select(a =>
new SelectListItem(a.StorageProvider().ToString(), a.StorageProvider().ToString())
);
return View(new ChooseStorageViewModel()
{
ProvidersList = providersList,
ShowChangeWarning = savedSettings != null,
Provider = savedSettings?.Provider ?? BTCPayServer.Storage.Models.StorageProvider.FileSystem
});

@ -29,10 +29,7 @@ namespace BTCPayServer.Controllers
var usersQuery = _UserManager.Users;
if (!string.IsNullOrWhiteSpace(model.SearchTerm))
{
#pragma warning disable CA1307 // Specify StringComparison
// Entity Framework don't support StringComparison
usersQuery = usersQuery.Where(u => u.Email.Contains(model.SearchTerm));
#pragma warning restore CA1307 // Specify StringComparison
}
if (sortOrder != null)

@ -31,7 +31,6 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Routing;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion.Internal;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NBitcoin;
@ -76,7 +75,7 @@ namespace BTCPayServer.Controllers
CheckConfigurationHostedService sshState,
EventAggregator eventAggregator,
CssThemeManager cssThemeManager,
IOptions<ExternalServicesOptions> externalServiceOptions)
IOptions<ExternalServicesOptions> externalServiceOptions)
{
_Options = options;
_StoredFileRepository = storedFileRepository;
@ -268,7 +267,7 @@ namespace BTCPayServer.Controllers
sshClient.Dispose();
}
}
public IHttpClientFactory HttpClientFactory { get; }
[Route("server/policies")]
@ -282,9 +281,9 @@ namespace BTCPayServer.Controllers
[Route("server/policies")]
[HttpPost]
public async Task<IActionResult> Policies([FromServices] BTCPayNetworkProvider btcPayNetworkProvider, PoliciesSettings settings, string command = "")
public async Task<IActionResult> Policies([FromServices] BTCPayNetworkProvider btcPayNetworkProvider,PoliciesSettings settings, string command = "")
{
ViewBag.UpdateUrlPresent = _Options.UpdateUrl != null;
ViewBag.AppsList = await GetAppSelectList();
if (command == "add-domain")
@ -302,7 +301,7 @@ namespace BTCPayServer.Controllers
}
settings.BlockExplorerLinks = settings.BlockExplorerLinks.Where(tuple => btcPayNetworkProvider.GetNetwork(tuple.CryptoCode).BlockExplorerLinkDefault != tuple.Link).ToList();
if (!ModelState.IsValid)
{
return View(settings);
@ -354,7 +353,7 @@ namespace BTCPayServer.Controllers
Link = this.Request.GetAbsoluteUriNoPathBase(externalService.Value).AbsoluteUri
});
}
if (await CanShowSSHService())
if (CanShowSSHService())
{
result.OtherExternalServices.Add(new ServicesViewModel.OtherExternalService()
{
@ -851,7 +850,7 @@ namespace BTCPayServer.Controllers
[Route("server/services/ssh")]
public async Task<IActionResult> SSHService()
{
if (!await CanShowSSHService())
if (!CanShowSSHService())
return NotFound();
var settings = _Options.SSHSettings;
@ -889,11 +888,9 @@ namespace BTCPayServer.Controllers
return View(vm);
}
async Task<bool> CanShowSSHService()
bool CanShowSSHService()
{
var policies = await _SettingsRepository.GetSettingAsync<PoliciesSettings>();
return !(policies?.DisableSSHService is true) &&
_Options.SSHSettings != null && (_sshState.CanUseSSH || CanAccessAuthorizedKeyFile());
return _Options.SSHSettings != null && (_sshState.CanUseSSH || CanAccessAuthorizedKeyFile());
}
private bool CanAccessAuthorizedKeyFile()
@ -903,88 +900,55 @@ namespace BTCPayServer.Controllers
[HttpPost]
[Route("server/services/ssh")]
public async Task<IActionResult> SSHService(SSHServiceViewModel viewModel, string command = null)
public async Task<IActionResult> SSHService(SSHServiceViewModel viewModel)
{
if (!await CanShowSSHService())
return NotFound();
string newContent = viewModel?.SSHKeyFileContent ?? string.Empty;
newContent = newContent.Replace("\r\n", "\n", StringComparison.OrdinalIgnoreCase);
if (command is "Save")
bool updated = false;
Exception exception = null;
// Let's try to just write the file
if (CanAccessAuthorizedKeyFile())
{
string newContent = viewModel?.SSHKeyFileContent ?? string.Empty;
newContent = newContent.Replace("\r\n", "\n", StringComparison.OrdinalIgnoreCase);
bool updated = false;
Exception exception = null;
// Let's try to just write the file
if (CanAccessAuthorizedKeyFile())
{
try
{
await System.IO.File.WriteAllTextAsync(_Options.SSHSettings.AuthorizedKeysFile, newContent);
TempData[WellKnownTempData.SuccessMessage] = "authorized_keys has been updated";
updated = true;
}
catch (Exception ex)
{
exception = ex;
}
}
// If that fail, fallback to ssh
if (!updated && _sshState.CanUseSSH)
{
try
{
using (var sshClient = await _Options.SSHSettings.ConnectAsync())
{
await sshClient.RunBash($"mkdir -p ~/.ssh && echo '{newContent.EscapeSingleQuotes()}' > ~/.ssh/authorized_keys", TimeSpan.FromSeconds(10));
}
updated = true;
exception = null;
}
catch (Exception ex)
{
exception = ex;
}
}
if (exception is null)
try
{
await System.IO.File.WriteAllTextAsync(_Options.SSHSettings.AuthorizedKeysFile, newContent);
TempData[WellKnownTempData.SuccessMessage] = "authorized_keys has been updated";
updated = true;
}
else
catch (Exception ex)
{
TempData[WellKnownTempData.ErrorMessage] = exception.Message;
exception = ex;
}
return RedirectToAction(nameof(SSHService));
}
else if (command is "disable")
{
return RedirectToAction(nameof(SSHServiceDisable));
}
return NotFound();
}
[Route("server/services/ssh/disable")]
public IActionResult SSHServiceDisable()
{
return View("Confirm", new ConfirmModel()
// If that fail, fallback to ssh
if (!updated && _sshState.CanUseSSH)
{
Action = "Disable",
Title = "Disable modification of SSH settings",
Description = "This action is permanent and will remove the ability to change the SSH settings via the BTCPay Server user interface.",
ButtonClass = "btn-danger"
});
}
[Route("server/services/ssh/disable")]
[HttpPost]
public async Task<IActionResult> SSHServiceDisablePost()
{
var policies = await _SettingsRepository.GetSettingAsync<PoliciesSettings>() ?? new PoliciesSettings();
policies.DisableSSHService = true;
await _SettingsRepository.UpdateSetting(policies);
TempData[WellKnownTempData.SuccessMessage] = "Changes to the SSH settings are now permanently disabled in the BTCPay Server user interface";
return RedirectToAction(nameof(Services));
try
{
using (var sshClient = await _Options.SSHSettings.ConnectAsync())
{
await sshClient.RunBash($"mkdir -p ~/.ssh && echo '{newContent.EscapeSingleQuotes()}' > ~/.ssh/authorized_keys", TimeSpan.FromSeconds(10));
}
updated = true;
exception = null;
}
catch (Exception ex)
{
exception = ex;
}
}
if (exception is null)
{
TempData[WellKnownTempData.SuccessMessage] = "authorized_keys has been updated";
}
else
{
TempData[WellKnownTempData.ErrorMessage] = exception.Message;
}
return RedirectToAction(nameof(SSHService));
}
[Route("server/theme")]
@ -1014,7 +978,7 @@ namespace BTCPayServer.Controllers
[HttpPost]
public async Task<IActionResult> Emails(EmailsViewModel model, string command)
{
if (command == "Test")
{
try

@ -1,27 +1,15 @@
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Client;
using BTCPayServer.Data;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization;
using BTCPayServer.Payments.CoinSwitch;
using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Plugins.CoinSwitch
namespace BTCPayServer.Controllers
{
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Route("plugins/{storeId}/coinswitch")]
public class CoinSwitchController : Controller
public partial class StoresController
{
private readonly StoreRepository _storeRepository;
public CoinSwitchController(StoreRepository storeRepository)
{
_storeRepository = storeRepository;
}
[HttpGet("")]
[HttpGet]
[Route("{storeId}/coinswitch")]
public IActionResult UpdateCoinSwitchSettings(string storeId)
{
var store = HttpContext.GetStoreData();
@ -34,7 +22,8 @@ namespace BTCPayServer.Plugins.CoinSwitch
private void SetExistingValues(StoreData store, UpdateCoinSwitchSettingsViewModel vm)
{
var existing = store.GetStoreBlob().GetCoinSwitchSettings();
var existing = store.GetStoreBlob().CoinSwitchSettings;
if (existing == null)
return;
vm.MerchantId = existing.MerchantId;
@ -43,7 +32,8 @@ namespace BTCPayServer.Plugins.CoinSwitch
vm.AmountMarkupPercentage = existing.AmountMarkupPercentage;
}
[HttpPost("")]
[HttpPost]
[Route("{storeId}/coinswitch")]
public async Task<IActionResult> UpdateCoinSwitchSettings(string storeId, UpdateCoinSwitchSettingsViewModel vm,
string command)
{
@ -70,11 +60,14 @@ namespace BTCPayServer.Plugins.CoinSwitch
{
case "save":
var storeBlob = store.GetStoreBlob();
storeBlob.SetCoinSwitchSettings(coinSwitchSettings);
storeBlob.CoinSwitchSettings = coinSwitchSettings;
store.SetStoreBlob(storeBlob);
await _storeRepository.UpdateStore(store);
await _Repo.UpdateStore(store);
TempData[WellKnownTempData.SuccessMessage] = "CoinSwitch settings modified";
return RedirectToAction(nameof(UpdateCoinSwitchSettings), new {storeId});
return RedirectToAction(nameof(UpdateStore), new
{
storeId
});
default:
return View(vm);

@ -15,7 +15,7 @@ namespace BTCPayServer.Controllers
public partial class StoresController
{
[HttpGet("{storeId}/lightning/{cryptoCode}")]
public IActionResult SetupLightningNode(string storeId, string cryptoCode)
public IActionResult AddLightningNode(string storeId, string cryptoCode)
{
var store = HttpContext.GetStoreData();
if (store == null)
@ -31,7 +31,7 @@ namespace BTCPayServer.Controllers
}
[HttpPost("{storeId}/lightning/{cryptoCode}")]
public async Task<IActionResult> SetupLightningNode(string storeId, LightningNodeViewModel vm, string command, string cryptoCode)
public async Task<IActionResult> AddLightningNode(string storeId, LightningNodeViewModel vm, string command, string cryptoCode)
{
vm.CryptoCode = cryptoCode;
var store = HttpContext.GetStoreData();
@ -48,8 +48,6 @@ namespace BTCPayServer.Controllers
}
var paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.LightningLike);
var lightning = GetExistingLightningSupportedPaymentMethod(vm.CryptoCode, store);
LightningSupportedPaymentMethod paymentMethod = null;
if (vm.LightningNodeType == LightningNodeType.Internal)
{
@ -98,11 +96,12 @@ namespace BTCPayServer.Controllers
{
case "save":
var storeBlob = store.GetStoreBlob();
storeBlob.SetExcluded(paymentMethodId, !vm.Enabled);
storeBlob.Hints.Lightning = false;
store.SetStoreBlob(storeBlob);
store.SetSupportedPaymentMethod(paymentMethodId, paymentMethod);
await _Repo.UpdateStore(store);
TempData[WellKnownTempData.SuccessMessage] = $"{network.CryptoCode} Lightning node updated.";
TempData[WellKnownTempData.SuccessMessage] = $"{network.CryptoCode} Lightning node modified.";
return RedirectToAction(nameof(UpdateStore), new { storeId });
case "test":
@ -115,7 +114,7 @@ namespace BTCPayServer.Controllers
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(20));
await handler.TestConnection(info, cts.Token);
}
TempData[WellKnownTempData.SuccessMessage] = $"Connection to the Lightning node successful. Your node address: {info}";
TempData[WellKnownTempData.SuccessMessage] = $"Connection to the Lightning node succeeded. Your node address: {info}";
}
catch (Exception ex)
{
@ -129,31 +128,6 @@ namespace BTCPayServer.Controllers
}
}
[HttpPost("{storeId}/lightning/{cryptoCode}/status")]
public async Task<IActionResult> SetLightningNodeEnabled(string storeId, string cryptoCode, bool enabled)
{
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
var network = cryptoCode == null ? null : _ExplorerProvider.GetNetwork(cryptoCode);
if (network == null)
return NotFound();
var lightning = GetExistingLightningSupportedPaymentMethod(cryptoCode, store);
if (lightning == null)
return NotFound();
var paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.LightningLike);
var storeBlob = store.GetStoreBlob();
storeBlob.SetExcluded(paymentMethodId, !enabled);
store.SetStoreBlob(storeBlob);
await _Repo.UpdateStore(store);
TempData[WellKnownTempData.SuccessMessage] = $"{network.CryptoCode} Lightning payments are now {(enabled ? "enabled" : "disabled")} for this store.";
return RedirectToAction(nameof(UpdateStore), new { storeId });
}
private bool CanUseInternalLightning()
{
return User.IsInRole(Roles.ServerAdmin) || _CssThemeManager.AllowLightningInternalNodeForAll;
@ -161,17 +135,14 @@ namespace BTCPayServer.Controllers
private void SetExistingValues(StoreData store, LightningNodeViewModel vm)
{
vm.CanUseInternalNode = CanUseInternalLightning();
var lightning = GetExistingLightningSupportedPaymentMethod(vm.CryptoCode, store);
if (lightning != null)
{
vm.LightningNodeType = lightning.IsInternalNode ? LightningNodeType.Internal : LightningNodeType.Custom;
vm.ConnectionString = lightning.GetDisplayableConnectionString();
}
else
{
vm.LightningNodeType = vm.CanUseInternalNode ? LightningNodeType.Internal : LightningNodeType.Custom;
}
vm.Enabled = !store.GetStoreBlob().IsExcluded(new PaymentMethodId(vm.CryptoCode, PaymentTypes.LightningLike)) && lightning != null;
vm.CanUseInternalNode = CanUseInternalLightning();
}
private LightningSupportedPaymentMethod GetExistingLightningSupportedPaymentMethod(string cryptoCode, StoreData store)

@ -157,7 +157,9 @@ namespace BTCPayServer.Controllers
var configChanged = oldConfig != vm.Config;
PaymentMethodId paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike);
var storeBlob = store.GetStoreBlob();
var wasExcluded = storeBlob.GetExcludedPaymentMethods().Match(paymentMethodId);
var willBeExcluded = !vm.Enabled;
var excludedChanged = willBeExcluded != wasExcluded;
var showAddress = // Show addresses if:
// - If the user is testing the hint address in confirmation screen
@ -186,7 +188,17 @@ namespace BTCPayServer.Controllers
await _Repo.UpdateStore(store);
_EventAggregator.Publish(new WalletChangedEvent {WalletId = new WalletId(vm.StoreId, vm.CryptoCode)});
TempData[WellKnownTempData.SuccessMessage] = $"Derivation settings for {network.CryptoCode} have been updated.";
if (excludedChanged)
{
var label = willBeExcluded ? "disabled" : "enabled";
TempData[WellKnownTempData.SuccessMessage] =
$"On-Chain payments for {network.CryptoCode} have been {label}.";
}
else
{
TempData[WellKnownTempData.SuccessMessage] =
$"Derivation settings for {network.CryptoCode} have been modified.";
}
// This is success case when derivation scheme is added to the store
return RedirectToAction(nameof(UpdateStore), new {storeId = vm.StoreId});
@ -377,7 +389,8 @@ namespace BTCPayServer.Controllers
return checkResult;
}
TempData[WellKnownTempData.SuccessMessage] = $"Derivation settings for {network.CryptoCode} have been updated.";
TempData[WellKnownTempData.SuccessMessage] =
$"Derivation settings for {network.CryptoCode} have been modified.";
return RedirectToAction(nameof(UpdateStore), new {storeId});
}
@ -493,40 +506,6 @@ namespace BTCPayServer.Controllers
});
}
[HttpPost("{storeId}/onchain/{cryptoCode}/status")]
public async Task<IActionResult> SetWalletEnabled(string storeId, string cryptoCode, bool enabled)
{
var checkResult = IsAvailable(cryptoCode, out var store, out var network);
if (checkResult != null)
{
return checkResult;
}
var derivation = GetExistingDerivationStrategy(cryptoCode, store);
if (derivation == null)
{
return NotFound();
}
var wallet = _WalletProvider.GetWallet(network);
if (wallet == null)
{
return NotFound();
}
var paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike);
var storeBlob = store.GetStoreBlob();
storeBlob.SetExcluded(paymentMethodId, !enabled);
store.SetStoreBlob(storeBlob);
await _Repo.UpdateStore(store);
_EventAggregator.Publish(new WalletChangedEvent {WalletId = new WalletId(storeId, cryptoCode)});
TempData[WellKnownTempData.SuccessMessage] =
$"{network.CryptoCode} on-chain payments are now {(enabled ? "enabled" : "disabled")} for this store.";
return RedirectToAction(nameof(UpdateStore), new {storeId});
}
[HttpPost("{storeId}/onchain/{cryptoCode}/delete")]
public async Task<IActionResult> ConfirmDeleteWallet(string storeId, string cryptoCode)
{

@ -544,16 +544,24 @@ namespace BTCPayServer.Controllers
break;
case LightningPaymentType _:
var lightning = lightningByCryptoCode.TryGet(paymentMethodId.CryptoCode);
var isEnabled = !excludeFilters.Match(paymentMethodId) && lightning != null;
vm.LightningNodes.Add(new StoreViewModel.LightningNode
vm.LightningNodes.Add(new StoreViewModel.LightningNode()
{
CryptoCode = paymentMethodId.CryptoCode,
Address = lightning?.GetDisplayableConnectionString(),
Enabled = isEnabled
Enabled = !excludeFilters.Match(paymentMethodId) && lightning != null
});
break;
}
}
var coinSwitchEnabled = storeBlob.CoinSwitchSettings != null && storeBlob.CoinSwitchSettings.Enabled;
vm.ThirdPartyPaymentMethods.Add(new StoreViewModel.AdditionalPaymentMethod()
{
Enabled = coinSwitchEnabled,
Action = nameof(UpdateCoinSwitchSettings),
Provider = "CoinSwitch"
});
}
@ -849,12 +857,6 @@ namespace BTCPayServer.Controllers
if (string.IsNullOrWhiteSpace(userId))
return Challenge(AuthenticationSchemes.Cookie);
var storeId = CurrentStore?.Id;
if (storeId != null)
{
var store = await _Repo.FindStore(storeId, userId);
if (store != null)
HttpContext.SetStoreData(store);
}
var model = new CreateTokenViewModel();
ViewBag.HidePublicKey = true;
ViewBag.ShowStores = true;
@ -911,14 +913,6 @@ namespace BTCPayServer.Controllers
return Challenge(AuthenticationSchemes.Cookie);
if (pairingCode == null)
return NotFound();
if (selectedStore != null)
{
var store = await _Repo.FindStore(selectedStore, userId);
if (store == null)
return NotFound();
HttpContext.SetStoreData(store);
ViewBag.ShowStores = false;
}
var pairing = await _TokenRepository.GetPairingAsync(pairingCode);
if (pairing == null)
{
@ -928,7 +922,7 @@ namespace BTCPayServer.Controllers
else
{
var stores = await _Repo.GetStoresByUserId(userId);
return View(new PairingModel
return View(new PairingModel()
{
Id = pairing.Id,
Label = pairing.Label,
@ -987,6 +981,8 @@ namespace BTCPayServer.Controllers
return _UserManager.GetUserId(User);
}
// TODO: Need to have talk about how architect default currency implementation
// For now we have also hardcoded USD for Store creation and then Invoice creation
const string DEFAULT_CURRENCY = "USD";
@ -1015,7 +1011,7 @@ namespace BTCPayServer.Controllers
ButtonType = 0,
Min = 1,
Max = 20,
Step = "1",
Step = 1,
Apps = apps
};
return View(model);

@ -6,7 +6,6 @@ using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.HostedServices;
using BTCPayServer.ModelBinders;
@ -18,7 +17,6 @@ using BTCPayServer.Views;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using NBitcoin;
using PayoutData = BTCPayServer.Data.PayoutData;
namespace BTCPayServer.Controllers
{
@ -191,9 +189,7 @@ namespace BTCPayServer.Controllers
var storeId = walletId.StoreId;
var paymentMethodId = new PaymentMethodId(walletId.CryptoCode, PaymentTypes.BTCLike);
var commandState = Enum.Parse<PayoutState>(vm.Command.Split("-").First());
var payoutIds = vm.GetSelectedPayouts(commandState);
var payoutIds = vm.WaitingForApproval.Where(p => p.Selected).Select(p => p.PayoutId).ToArray();
if (payoutIds.Length == 0)
{
this.TempData.SetStatusMessageModel(new StatusMessageModel()
@ -207,121 +203,93 @@ namespace BTCPayServer.Controllers
pullPaymentId = vm.PullPaymentId
});
}
var command = vm.Command.Substring(vm.Command.IndexOf('-', StringComparison.InvariantCulture) + 1);
switch (command)
if (vm.Command == "pay")
{
case "approve-pay":
case "approve":
{
await using var ctx = this._dbContextFactory.CreateContext();
ctx.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
var payouts = await GetPayoutsForPaymentMethod(walletId.GetPaymentMethodId(), ctx, payoutIds, storeId, cancellationToken);
for (int i = 0; i < payouts.Count; i++)
{
var payout = payouts[i];
if (payout.State != PayoutState.AwaitingApproval)
continue;
var rateResult = await _pullPaymentService.GetRate(payout, null, cancellationToken);
if (rateResult.BidAsk == null)
{
this.TempData.SetStatusMessageModel(new StatusMessageModel()
{
Message = $"Rate unavailable: {rateResult.EvaluatedRule}",
Severity = StatusMessageModel.StatusSeverity.Error
});
return RedirectToAction(nameof(Payouts), new
{
walletId = walletId.ToString(),
pullPaymentId = vm.PullPaymentId
});
}
var approveResult = await _pullPaymentService.Approve(new HostedServices.PullPaymentHostedService.PayoutApproval()
{
PayoutId = payout.Id,
Revision = payout.GetBlob(_jsonSerializerSettings).Revision,
Rate = rateResult.BidAsk.Ask
});
if (approveResult != HostedServices.PullPaymentHostedService.PayoutApproval.Result.Ok)
{
this.TempData.SetStatusMessageModel(new StatusMessageModel()
{
Message = PullPaymentHostedService.PayoutApproval.GetErrorMessage(approveResult),
Severity = StatusMessageModel.StatusSeverity.Error
});
return RedirectToAction(nameof(Payouts), new
{
walletId = walletId.ToString(),
pullPaymentId = vm.PullPaymentId
});
}
}
if (command == "approve-pay")
{
goto case "pay";
}
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Message = "Payouts approved", Severity = StatusMessageModel.StatusSeverity.Success
});
return RedirectToAction(nameof(Payouts),
new {walletId = walletId.ToString(), pullPaymentId = vm.PullPaymentId});
}
case "pay":
{
await using var ctx = this._dbContextFactory.CreateContext();
ctx.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
var payouts = await GetPayoutsForPaymentMethod(walletId.GetPaymentMethodId(), ctx, payoutIds, storeId, cancellationToken);
var walletSend = (WalletSendModel)((ViewResult)(await this.WalletSend(walletId))).Model;
walletSend.Outputs.Clear();
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode);
List<string> bip21 = new List<string>();
foreach (var payout in payouts)
{
var blob = payout.GetBlob(_jsonSerializerSettings);
if (payout.GetPaymentMethodId() != paymentMethodId)
continue;
bip21.Add(network.GenerateBIP21(payout.Destination, new Money(blob.CryptoAmount.Value, MoneyUnit.BTC)));
}
return RedirectToAction(nameof(WalletSend), new {walletId, bip21});
}
case "cancel":
await _pullPaymentService.Cancel(
new HostedServices.PullPaymentHostedService.CancelRequest(payoutIds));
this.TempData.SetStatusMessageModel(new StatusMessageModel()
{
Message = "Payouts archived", Severity = StatusMessageModel.StatusSeverity.Success
});
return RedirectToAction(nameof(Payouts),
new {walletId = walletId.ToString(), pullPaymentId = vm.PullPaymentId});
}
return NotFound();
}
private static async Task<List<PayoutData>> GetPayoutsForPaymentMethod(PaymentMethodId paymentMethodId,
ApplicationDbContext ctx, string[] payoutIds,
string storeId, CancellationToken cancellationToken)
{
var payouts = (await ctx.Payouts
using var ctx = this._dbContextFactory.CreateContext();
ctx.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
var payouts = (await ctx.Payouts
.Include(p => p.PullPaymentData)
.Include(p => p.PullPaymentData.StoreData)
.Where(p => payoutIds.Contains(p.Id))
.Where(p => p.PullPaymentData.StoreId == storeId && !p.PullPaymentData.Archived)
.ToListAsync(cancellationToken))
.Where(p => p.GetPaymentMethodId() == paymentMethodId)
.ToList();
return payouts;
.ToListAsync())
.Where(p => p.GetPaymentMethodId() == walletId.GetPaymentMethodId())
.ToList();
for (int i = 0; i < payouts.Count; i++)
{
var payout = payouts[i];
if (payout.State != PayoutState.AwaitingApproval)
continue;
var rateResult = await _pullPaymentService.GetRate(payout, null, cancellationToken);
if (rateResult.BidAsk == null)
{
this.TempData.SetStatusMessageModel(new StatusMessageModel()
{
Message = $"Rate unavailable: {rateResult.EvaluatedRule}",
Severity = StatusMessageModel.StatusSeverity.Error
});
return RedirectToAction(nameof(Payouts), new
{
walletId = walletId.ToString(),
pullPaymentId = vm.PullPaymentId
});
}
var approveResult = await _pullPaymentService.Approve(new HostedServices.PullPaymentHostedService.PayoutApproval()
{
PayoutId = payout.Id,
Revision = payout.GetBlob(_jsonSerializerSettings).Revision,
Rate = rateResult.BidAsk.Ask
});
if (approveResult != HostedServices.PullPaymentHostedService.PayoutApproval.Result.Ok)
{
this.TempData.SetStatusMessageModel(new StatusMessageModel()
{
Message = PullPaymentHostedService.PayoutApproval.GetErrorMessage(approveResult),
Severity = StatusMessageModel.StatusSeverity.Error
});
return RedirectToAction(nameof(Payouts), new
{
walletId = walletId.ToString(),
pullPaymentId = vm.PullPaymentId
});
}
payouts[i] = await ctx.Payouts.FindAsync(payouts[i].Id);
}
var walletSend = (WalletSendModel)((ViewResult)(await this.WalletSend(walletId))).Model;
walletSend.Outputs.Clear();
foreach (var payout in payouts)
{
var blob = payout.GetBlob(_jsonSerializerSettings);
if (payout.GetPaymentMethodId() != paymentMethodId)
continue;
var output = new WalletSendModel.TransactionOutput()
{
Amount = blob.CryptoAmount,
DestinationAddress = blob.Destination.Address.ToString()
};
walletSend.Outputs.Add(output);
}
return View(nameof(walletSend), walletSend);
}
else if (vm.Command == "cancel")
{
await _pullPaymentService.Cancel(new HostedServices.PullPaymentHostedService.CancelRequest(payoutIds));
this.TempData.SetStatusMessageModel(new StatusMessageModel()
{
Message = "Payouts archived",
Severity = StatusMessageModel.StatusSeverity.Success
});
return RedirectToAction(nameof(Payouts), new
{
walletId = walletId.ToString(),
pullPaymentId = vm.PullPaymentId
});
}
else
{
return NotFound();
}
}
[HttpGet]
@ -331,11 +299,9 @@ namespace BTCPayServer.Controllers
WalletId walletId, PayoutsModel vm = null)
{
vm ??= new PayoutsModel();
vm.PayoutStateSets ??= ((PayoutState[]) Enum.GetValues(typeof(PayoutState))).Select(state =>
new PayoutsModel.PayoutStateSet() {State = state, Payouts = new List<PayoutsModel.PayoutModel>()}).ToList();
using var ctx = this._dbContextFactory.CreateContext();
var storeId = walletId.StoreId;
vm.PaymentMethodId = new PaymentMethodId(walletId.CryptoCode, PaymentTypes.BTCLike);
var paymentMethodId = new PaymentMethodId(walletId.CryptoCode, PaymentTypes.BTCLike);
var payoutRequest = ctx.Payouts.Where(p => p.PullPaymentData.StoreId == storeId && !p.PullPaymentData.Archived);
if (vm.PullPaymentId != null)
{
@ -347,42 +313,34 @@ namespace BTCPayServer.Controllers
Payout = o,
PullPayment = o.PullPaymentData
}).ToListAsync();
foreach (var stateSet in payouts.GroupBy(arg => arg.Payout.State))
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode);
vm.WaitingForApproval = new List<PayoutsModel.PayoutModel>();
vm.Other = new List<PayoutsModel.PayoutModel>();
foreach (var item in payouts)
{
var state = vm.PayoutStateSets.SingleOrDefault(set => set.State == stateSet.Key);
if (state == null)
if (item.Payout.GetPaymentMethodId() != paymentMethodId)
continue;
var ppBlob = item.PullPayment.GetBlob();
var payoutBlob = item.Payout.GetBlob(_jsonSerializerSettings);
var m = new PayoutsModel.PayoutModel();
m.PullPaymentId = item.PullPayment.Id;
m.PullPaymentName = ppBlob.Name ?? item.PullPayment.Id;
m.Date = item.Payout.Date;
m.PayoutId = item.Payout.Id;
m.Amount = _currencyTable.DisplayFormatCurrency(payoutBlob.Amount, ppBlob.Currency);
m.Destination = payoutBlob.Destination.Address.ToString();
if (item.Payout.State == PayoutState.AwaitingPayment || item.Payout.State == PayoutState.AwaitingApproval)
{
state = new PayoutsModel.PayoutStateSet()
{
Payouts = new List<PayoutsModel.PayoutModel>(), State = stateSet.Key
};
vm.PayoutStateSets.Add(state);
vm.WaitingForApproval.Add(m);
}
foreach (var item in stateSet)
else
{
if (item.Payout.GetPaymentMethodId() != vm.PaymentMethodId)
continue;
var ppBlob = item.PullPayment.GetBlob();
var payoutBlob = item.Payout.GetBlob(_jsonSerializerSettings);
var m = new PayoutsModel.PayoutModel();
m.PullPaymentId = item.PullPayment.Id;
m.PullPaymentName = ppBlob.Name ?? item.PullPayment.Id;
m.Date = item.Payout.Date;
m.PayoutId = item.Payout.Id;
m.Amount = _currencyTable.DisplayFormatCurrency(payoutBlob.Amount, ppBlob.Currency);
m.Destination = payoutBlob.Destination;
var handler = _payoutHandlers
.FirstOrDefault(handler => handler.CanHandle(item.Payout.GetPaymentMethodId()));
var proofBlob = handler?.ParseProof(item.Payout);
m.TransactionLink = proofBlob?.Link;
state.Payouts.Add(m);
if (item.Payout.GetPaymentMethodId().PaymentType == PaymentTypes.BTCLike &&
item.Payout.GetProofBlob(this._jsonSerializerSettings)?.TransactionId is uint256 txId)
m.TransactionLink = string.Format(CultureInfo.InvariantCulture, network.BlockExplorerLink, txId);
vm.Other.Add(m);
}
}
vm.PayoutStateSets = vm.PayoutStateSets.Where(set => set.Payouts?.Any() is true).ToList();
return View(vm);
}
}

@ -8,6 +8,7 @@ using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.HostedServices;
using BTCPayServer.ModelBinders;
@ -15,6 +16,7 @@ using BTCPayServer.Models;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Models.WalletViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Security;
using BTCPayServer.Services;
using BTCPayServer.Services.Labels;
using BTCPayServer.Services.Rates;
@ -60,7 +62,6 @@ namespace BTCPayServer.Controllers
private readonly ApplicationDbContextFactory _dbContextFactory;
private readonly BTCPayNetworkJsonSerializerSettings _jsonSerializerSettings;
private readonly PullPaymentHostedService _pullPaymentService;
private readonly IEnumerable<IPayoutHandler> _payoutHandlers;
public RateFetcher RateFetcher { get; }
@ -85,8 +86,7 @@ namespace BTCPayServer.Controllers
LabelFactory labelFactory,
ApplicationDbContextFactory dbContextFactory,
BTCPayNetworkJsonSerializerSettings jsonSerializerSettings,
HostedServices.PullPaymentHostedService pullPaymentService,
IEnumerable<IPayoutHandler> payoutHandlers)
HostedServices.PullPaymentHostedService pullPaymentService)
{
_currencyTable = currencyTable;
Repository = repo;
@ -109,7 +109,6 @@ namespace BTCPayServer.Controllers
_dbContextFactory = dbContextFactory;
_jsonSerializerSettings = jsonSerializerSettings;
_pullPaymentService = pullPaymentService;
_payoutHandlers = payoutHandlers;
}
// Borrowed from https://github.com/ManageIQ/guides/blob/master/labels.md
@ -343,8 +342,6 @@ namespace BTCPayServer.Controllers
model.Transactions = model.Transactions.OrderByDescending(t => t.Timestamp).Skip(skip).Take(count).ToList();
}
model.CryptoCode = walletId.CryptoCode;
return View(model);
}
@ -427,7 +424,7 @@ namespace BTCPayServer.Controllers
[Route("{walletId}/send")]
public async Task<IActionResult> WalletSend(
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, string defaultDestination = null, string defaultAmount = null, string[] bip21 = null)
WalletId walletId, string defaultDestination = null, string defaultAmount = null, string bip21 = null)
{
if (walletId?.StoreId == null)
return NotFound();
@ -445,29 +442,19 @@ namespace BTCPayServer.Controllers
double.TryParse(defaultAmount, out var amount);
var model = new WalletSendModel()
{
CryptoCode = walletId.CryptoCode
};
if (bip21?.Any() is true)
{
foreach (var link in bip21)
{
if (!string.IsNullOrEmpty(link))
{
LoadFromBIP21(model, link, network);
}
}
}
if (!(model.Outputs?.Any() is true))
{
model.Outputs = new List<WalletSendModel.TransactionOutput>()
Outputs = new List<WalletSendModel.TransactionOutput>()
{
new WalletSendModel.TransactionOutput()
{
Amount = Convert.ToDecimal(amount), DestinationAddress = defaultDestination
Amount = Convert.ToDecimal(amount),
DestinationAddress = defaultDestination
}
};
},
CryptoCode = walletId.CryptoCode
};
if (!string.IsNullOrEmpty(bip21))
{
LoadFromBIP21(model, bip21, network);
}
var feeProvider = _feeRateProvider.CreateFeeProvider(network);
var recommendedFees =
@ -551,7 +538,6 @@ namespace BTCPayServer.Controllers
vm.NBXSeedAvailable = await GetSeed(walletId, network) != null;
if (!string.IsNullOrEmpty(bip21))
{
vm.Outputs?.Clear();
LoadFromBIP21(vm, bip21, network);
}
@ -576,8 +562,7 @@ namespace BTCPayServer.Controllers
Amount = coin.Value.GetValue(network),
Comment = info?.Comment,
Labels = info == null ? null : _labelFactory.ColorizeTransactionLabels(walletBlobAsync, info, Request),
Link = string.Format(CultureInfo.InvariantCulture, network.BlockExplorerLink, coin.OutPoint.Hash.ToString()),
Confirmations = coin.Confirmations
Link = string.Format(CultureInfo.InvariantCulture, network.BlockExplorerLink, coin.OutPoint.Hash.ToString())
};
}).ToArray();
}
@ -590,10 +575,6 @@ namespace BTCPayServer.Controllers
if (!string.IsNullOrEmpty(bip21))
{
if (!vm.Outputs.Any())
{
vm.Outputs.Add(new WalletSendModel.TransactionOutput());
}
return View(vm);
}
if (command == "add-output")
@ -737,7 +718,6 @@ namespace BTCPayServer.Controllers
private void LoadFromBIP21(WalletSendModel vm, string bip21, BTCPayNetwork network)
{
vm.Outputs ??= new List<WalletSendModel.TransactionOutput>();
try
{
if (bip21.StartsWith(network.UriScheme, StringComparison.InvariantCultureIgnoreCase))
@ -746,13 +726,15 @@ namespace BTCPayServer.Controllers
}
var uriBuilder = new NBitcoin.Payment.BitcoinUrlBuilder(bip21, network.NBitcoinNetwork);
vm.Outputs.Add(new WalletSendModel.TransactionOutput()
vm.Outputs = new List<WalletSendModel.TransactionOutput>()
{
Amount = uriBuilder.Amount?.ToDecimal(MoneyUnit.BTC),
DestinationAddress = uriBuilder.Address.ToString(),
SubtractFeesFromOutput = false
});
new WalletSendModel.TransactionOutput()
{
Amount = uriBuilder.Amount?.ToDecimal(MoneyUnit.BTC),
DestinationAddress = uriBuilder.Address.ToString(),
SubtractFeesFromOutput = false
}
};
if (!string.IsNullOrEmpty(uriBuilder.Label) || !string.IsNullOrEmpty(uriBuilder.Message))
{
TempData.SetStatusMessageModel(new StatusMessageModel()
@ -770,11 +752,13 @@ namespace BTCPayServer.Controllers
{
try
{
vm.Outputs.Add(new WalletSendModel.TransactionOutput()
vm.Outputs = new List<WalletSendModel.TransactionOutput>()
{
new WalletSendModel.TransactionOutput()
{
DestinationAddress = BitcoinAddress.Create(bip21, network.NBitcoinNetwork).ToString()
}
);
};
}
catch
{

@ -1,22 +0,0 @@
using System;
using NBitcoin;
namespace BTCPayServer.Data
{
public class AddressClaimDestination : IBitcoinLikeClaimDestination
{
public BitcoinAddress _bitcoinAddress;
public AddressClaimDestination(BitcoinAddress bitcoinAddress)
{
if (bitcoinAddress == null)
throw new ArgumentNullException(nameof(bitcoinAddress));
_bitcoinAddress = bitcoinAddress;
}
public BitcoinAddress Address => _bitcoinAddress;
public override string ToString()
{
return _bitcoinAddress.ToString();
}
}
}

@ -1,278 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.HostedServices;
using BTCPayServer.Logging;
using BTCPayServer.Payments;
using BTCPayServer.Services;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using NBitcoin;
using NBitcoin.Payment;
using NBitcoin.RPC;
using NBXplorer.Models;
using Newtonsoft.Json;
using NewBlockEvent = BTCPayServer.Events.NewBlockEvent;
using PayoutData = BTCPayServer.Data.PayoutData;
public class BitcoinLikePayoutHandler : IPayoutHandler
{
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
private readonly ExplorerClientProvider _explorerClientProvider;
private readonly BTCPayNetworkJsonSerializerSettings _jsonSerializerSettings;
private readonly ApplicationDbContextFactory _dbContextFactory;
private readonly EventAggregator _eventAggregator;
public BitcoinLikePayoutHandler(BTCPayNetworkProvider btcPayNetworkProvider,
ExplorerClientProvider explorerClientProvider, BTCPayNetworkJsonSerializerSettings jsonSerializerSettings,
ApplicationDbContextFactory dbContextFactory, EventAggregator eventAggregator)
{
_btcPayNetworkProvider = btcPayNetworkProvider;
_explorerClientProvider = explorerClientProvider;
_jsonSerializerSettings = jsonSerializerSettings;
_dbContextFactory = dbContextFactory;
_eventAggregator = eventAggregator;
}
public bool CanHandle(PaymentMethodId paymentMethod)
{
return paymentMethod.PaymentType == BitcoinPaymentType.Instance &&
_btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(paymentMethod.CryptoCode)?.ReadonlyWallet is false;
}
public Task<IClaimDestination> ParseClaimDestination(PaymentMethodId paymentMethodId, string destination)
{
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(paymentMethodId.CryptoCode);
destination = destination.Trim();
try
{
if (destination.StartsWith($"{network.UriScheme}:", StringComparison.OrdinalIgnoreCase))
{
return Task.FromResult<IClaimDestination>(new UriClaimDestination(new BitcoinUrlBuilder(destination, network.NBitcoinNetwork)));
}
return Task.FromResult<IClaimDestination>(new AddressClaimDestination(BitcoinAddress.Create(destination, network.NBitcoinNetwork)));
}
catch
{
return Task.FromResult<IClaimDestination>(null);
}
}
public IPayoutProof ParseProof(PayoutData payout)
{
if (payout?.Proof is null)
return null;
var paymentMethodId = payout.GetPaymentMethodId();
var res = JsonConvert.DeserializeObject<PayoutTransactionOnChainBlob>(Encoding.UTF8.GetString(payout.Proof), _jsonSerializerSettings.GetSerializer(paymentMethodId.CryptoCode));
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(paymentMethodId.CryptoCode);
res.LinkTemplate = network.BlockExplorerLink;
return res;
}
public void StartBackgroundCheck(Action<Type[]> subscribe)
{
subscribe(new[] {typeof(NewOnChainTransactionEvent), typeof(NewBlockEvent)});
}
public async Task BackgroundCheck(object o)
{
if (o is NewOnChainTransactionEvent newTransaction)
{
await UpdatePayoutsAwaitingForPayment(newTransaction);
}
if (o is NewBlockEvent || o is NewOnChainTransactionEvent)
{
await UpdatePayoutsInProgress();
}
}
public Task<decimal> GetMinimumPayoutAmount(PaymentMethodId paymentMethodId, IClaimDestination claimDestination)
{
if (_btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(paymentMethodId.CryptoCode)?
.NBitcoinNetwork?
.Consensus?
.ConsensusFactory?
.CreateTxOut() is TxOut txout &&
claimDestination is IBitcoinLikeClaimDestination bitcoinLikeClaimDestination)
{
txout.ScriptPubKey = bitcoinLikeClaimDestination.Address.ScriptPubKey;
return Task.FromResult(txout.GetDustThreshold(new FeeRate(1.0m)).ToDecimal(MoneyUnit.BTC));
}
return Task.FromResult(0m);
}
private async Task UpdatePayoutsInProgress()
{
try
{
using var ctx = _dbContextFactory.CreateContext();
var payouts = await ctx.Payouts
.Include(p => p.PullPaymentData)
.Where(p => p.State == PayoutState.InProgress)
.ToListAsync();
foreach (var payout in payouts)
{
var proof = ParseProof(payout) as PayoutTransactionOnChainBlob;
var payoutBlob = payout.GetBlob(this._jsonSerializerSettings);
if (proof is null || proof.Accounted is false)
{
continue;
}
foreach (var txid in proof.Candidates.ToList())
{
var explorer = _explorerClientProvider.GetExplorerClient(payout.GetPaymentMethodId().CryptoCode);
var tx = await explorer.GetTransactionAsync(txid);
if (tx is null)
{
proof.Candidates.Remove(txid);
}
else if (tx.Confirmations >= payoutBlob.MinimumConfirmation)
{
payout.State = PayoutState.Completed;
proof.TransactionId = tx.TransactionHash;
payout.Destination = null;
break;
}
else
{
var rebroadcasted = await explorer.BroadcastAsync(tx.Transaction);
if (rebroadcasted.RPCCode == RPCErrorCode.RPC_TRANSACTION_ERROR ||
rebroadcasted.RPCCode == RPCErrorCode.RPC_TRANSACTION_REJECTED)
{
proof.Candidates.Remove(txid);
}
else
{
payout.State = PayoutState.InProgress;
proof.TransactionId = tx.TransactionHash;
continue;
}
}
}
if (proof.TransactionId is null && !proof.Candidates.Contains(proof.TransactionId))
{
proof.TransactionId = null;
}
if (proof.Candidates.Count == 0)
{
payout.State = PayoutState.AwaitingPayment;
}
else if (proof.TransactionId is null)
{
proof.TransactionId = proof.Candidates.First();
}
if (payout.State == PayoutState.Completed)
proof.Candidates = null;
SetProofBlob(payout, proof);
}
await ctx.SaveChangesAsync();
}
catch (Exception ex)
{
Logs.PayServer.LogWarning(ex, "Error while processing an update in the pull payment hosted service");
}
}
private async Task UpdatePayoutsAwaitingForPayment(NewOnChainTransactionEvent newTransaction)
{
try
{
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(newTransaction.CryptoCode);
Dictionary<string, decimal> destinations;
if (newTransaction.NewTransactionEvent.TrackedSource is AddressTrackedSource addressTrackedSource)
{
destinations = new Dictionary<string, decimal>()
{
{
addressTrackedSource.Address.ToString(),
newTransaction.NewTransactionEvent.Outputs.Sum(output => output.Value.GetValue(network))
}
};
}
else
{
destinations = newTransaction.NewTransactionEvent.TransactionData.Transaction.Outputs
.GroupBy(txout => txout.ScriptPubKey)
.ToDictionary(
txoutSet => txoutSet.Key.GetDestinationAddress(network.NBitcoinNetwork).ToString(),
txoutSet => txoutSet.Sum(txout => txout.Value.ToDecimal(MoneyUnit.BTC)));
}
var paymentMethodId = new PaymentMethodId(newTransaction.CryptoCode, BitcoinPaymentType.Instance);
using var ctx = _dbContextFactory.CreateContext();
var payouts = await ctx.Payouts
.Include(o => o.PullPaymentData)
.Where(p => p.State == PayoutState.AwaitingPayment)
.Where(p => p.PaymentMethodId == paymentMethodId.ToString())
.Where(p => destinations.Keys.Contains(p.Destination))
.ToListAsync();
var payoutByDestination = payouts.ToDictionary(p => p.Destination);
foreach (var destination in destinations)
{
if (!payoutByDestination.TryGetValue(destination.Key, out var payout))
continue;
var payoutBlob = payout.GetBlob(_jsonSerializerSettings);
if (payoutBlob.CryptoAmount is null ||
// The round up here is not strictly necessary, this is temporary to fix existing payout before we
// were properly roundup the crypto amount
destination.Value != BTCPayServer.Extensions.RoundUp(payoutBlob.CryptoAmount.Value, network.Divisibility))
continue;
var proof = ParseProof(payout) as PayoutTransactionOnChainBlob;
if (proof is null)
{
proof = new PayoutTransactionOnChainBlob()
{
Accounted = !(newTransaction.NewTransactionEvent.TrackedSource is AddressTrackedSource ),
};
}
var txId = newTransaction.NewTransactionEvent.TransactionData.TransactionHash;
if (proof.Candidates.Add(txId))
{
if (proof.Accounted is true)
{
payout.State = PayoutState.InProgress;
var walletId = new WalletId(payout.PullPaymentData.StoreId, newTransaction.CryptoCode);
_eventAggregator.Publish(new UpdateTransactionLabel(walletId,
newTransaction.NewTransactionEvent.TransactionData.TransactionHash,
UpdateTransactionLabel.PayoutTemplate(payout.Id,payout.PullPaymentDataId, walletId.ToString())));
}
if (proof.TransactionId is null)
proof.TransactionId = txId;
SetProofBlob(payout, proof);
}
}
await ctx.SaveChangesAsync();
}
catch (Exception ex)
{
Logs.PayServer.LogWarning(ex, "Error while processing a transaction in the pull payment hosted service");
}
}
private void SetProofBlob(PayoutData data, PayoutTransactionOnChainBlob blob)
{
var bytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(blob, _jsonSerializerSettings.GetSerializer(data.GetPaymentMethodId().CryptoCode)));
// We only update the property if the bytes actually changed, this prevent from hammering the DB too much
if (data.Proof is null || bytes.Length != data.Proof.Length || !bytes.SequenceEqual(data.Proof))
{
data.Proof = bytes;
}
}
}

@ -1,9 +0,0 @@
using NBitcoin;
namespace BTCPayServer.Data
{
public interface IBitcoinLikeClaimDestination : IClaimDestination
{
BitcoinAddress Address { get; }
}
}

@ -1,25 +0,0 @@
using System.Collections.Generic;
using System.Globalization;
using NBitcoin;
using Newtonsoft.Json;
namespace BTCPayServer.Data
{
public class PayoutTransactionOnChainBlob: IPayoutProof
{
[JsonConverter(typeof(NBitcoin.JsonConverters.UInt256JsonConverter))]
public uint256 TransactionId { get; set; }
[JsonProperty(ItemConverterType = typeof(NBitcoin.JsonConverters.UInt256JsonConverter), NullValueHandling = NullValueHandling.Ignore)]
public HashSet<uint256> Candidates { get; set; } = new HashSet<uint256>();
[JsonIgnore] public string LinkTemplate { get; set; }
[JsonIgnore]
public string Link
{
get { return Id != null ? string.Format(CultureInfo.InvariantCulture, LinkTemplate, Id) : null; }
}
public bool? Accounted { get; set; }//nullable to be backwards compatible. if null, accounted is true
[JsonIgnore]
public string Id { get { return TransactionId?.ToString(); } }
}
}

@ -1,27 +0,0 @@
using System;
using NBitcoin;
using NBitcoin.Payment;
namespace BTCPayServer.Data
{
public class UriClaimDestination : IBitcoinLikeClaimDestination
{
private readonly BitcoinUrlBuilder _bitcoinUrl;
public UriClaimDestination(BitcoinUrlBuilder bitcoinUrl)
{
if (bitcoinUrl == null)
throw new ArgumentNullException(nameof(bitcoinUrl));
if (bitcoinUrl.Address is null)
throw new ArgumentException(nameof(bitcoinUrl));
_bitcoinUrl = bitcoinUrl;
}
public BitcoinUrlBuilder BitcoinUrl => _bitcoinUrl;
public BitcoinAddress Address => _bitcoinUrl.Address;
public override string ToString()
{
return _bitcoinUrl.ToString();
}
}
}

@ -1,12 +0,0 @@
namespace BTCPayServer.Data
{
public interface IClaimDestination
{
}
public interface IPayoutProof
{
string Link { get; }
string Id { get; }
}
}

@ -1,17 +0,0 @@
using System;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Payments;
public interface IPayoutHandler
{
public bool CanHandle(PaymentMethodId paymentMethod);
//Allows payout handler to parse payout destinations on its own
public Task<IClaimDestination> ParseClaimDestination(PaymentMethodId paymentMethodId, string destination);
public IPayoutProof ParseProof(PayoutData payout);
//Allows you to subscribe the main pull payment hosted service to events and prepare the handler
void StartBackgroundCheck(Action<Type[]> subscribe);
//allows you to process events that the main pull payment hosted service is subscribed to
Task BackgroundCheck(object o);
Task<decimal> GetMinimumPayoutAmount(PaymentMethodId paymentMethod, IClaimDestination claimDestination);
}

@ -1,16 +0,0 @@
using BTCPayServer.JsonConverters;
using Newtonsoft.Json;
namespace BTCPayServer.Data
{
public class PayoutBlob
{
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal Amount { get; set; }
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal? CryptoAmount { get; set; }
public int MinimumConfirmation { get; set; } = 1;
public string Destination { get; set; }
public int Revision { get; set; }
}
}

@ -1,42 +0,0 @@
using System;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
using BTCPayServer.Payments;
using BTCPayServer.Services;
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json;
namespace BTCPayServer.Data
{
public static class PayoutExtensions
{
public static async Task<PayoutData> GetPayout(this DbSet<PayoutData> payouts, string payoutId, string storeId, bool includePullPayment = false, bool includeStore = false)
{
IQueryable<PayoutData> query = payouts;
if (includePullPayment)
query = query.Include(p => p.PullPaymentData);
if (includeStore)
query = query.Include(p => p.PullPaymentData.StoreData);
var payout = await query.Where(p => p.Id == payoutId &&
p.PullPaymentData.StoreId == storeId).FirstOrDefaultAsync();
if (payout is null)
return null;
return payout;
}
public static PaymentMethodId GetPaymentMethodId(this PayoutData data)
{
return PaymentMethodId.Parse(data.PaymentMethodId);
}
public static PayoutBlob GetBlob(this PayoutData data, BTCPayNetworkJsonSerializerSettings serializers)
{
return JsonConvert.DeserializeObject<PayoutBlob>(Encoding.UTF8.GetString(data.Blob), serializers.GetSerializer(data.GetPaymentMethodId().CryptoCode));
}
public static void SetBlob(this PayoutData data, PayoutBlob blob, BTCPayNetworkJsonSerializerSettings serializers)
{
data.Blob = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(blob, serializers.GetSerializer(data.GetPaymentMethodId().CryptoCode)));
}
}
}

@ -1,34 +0,0 @@
using System;
using BTCPayServer.Client.JsonConverters;
using BTCPayServer.JsonConverters;
using BTCPayServer.Payments;
using Newtonsoft.Json;
namespace BTCPayServer.Data
{
public class PullPaymentBlob
{
public string Name { get; set; }
public string Currency { get; set; }
public int Divisibility { get; set; }
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal Limit { get; set; }
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal MinimumClaim { get; set; }
public PullPaymentView View { get; set; } = new PullPaymentView();
[JsonConverter(typeof(TimeSpanJsonConverter.Seconds))]
public TimeSpan? Period { get; set; }
[JsonProperty(ItemConverterType = typeof(PaymentMethodIdJsonConverter))]
public PaymentMethodId[] SupportedPaymentMethods { get; set; }
public class PullPaymentView
{
public string Title { get; set; }
public string Description { get; set; }
public string EmbeddedCSS { get; set; }
public string Email { get; set; }
public string CustomCSSLink { get; set; }
}
}
}

@ -1,25 +0,0 @@
using System.Linq;
using System.Text;
using NBitcoin.JsonConverters;
using Newtonsoft.Json;
namespace BTCPayServer.Data
{
public static class PullPaymentsExtensions
{
public static PullPaymentBlob GetBlob(this PullPaymentData data)
{
return JsonConvert.DeserializeObject<PullPaymentBlob>(Encoding.UTF8.GetString(data.Blob));
}
public static void SetBlob(this PullPaymentData data, PullPaymentBlob blob)
{
data.Blob = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(blob));
}
public static bool IsSupported(this PullPaymentData data, BTCPayServer.Payments.PaymentMethodId paymentId)
{
return data.GetBlob().SupportedPaymentMethods.Contains(paymentId);
}
}
}

@ -0,0 +1,212 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Client.JsonConverters;
using BTCPayServer.JsonConverters;
using BTCPayServer.Payments;
using BTCPayServer.Services;
using Microsoft.EntityFrameworkCore;
using NBitcoin;
using NBitcoin.JsonConverters;
using NBitcoin.Payment;
using Newtonsoft.Json;
namespace BTCPayServer.Data
{
public static class PullPaymentsExtensions
{
public static async Task<PayoutData> GetPayout(this DbSet<PayoutData> payouts, string payoutId, string storeId, bool includePullPayment = false, bool includeStore = false)
{
IQueryable<PayoutData> query = payouts;
if (includePullPayment)
query = query.Include(p => p.PullPaymentData);
if (includeStore)
query = query.Include(p => p.PullPaymentData.StoreData);
var payout = await query.Where(p => p.Id == payoutId &&
p.PullPaymentData.StoreId == storeId).FirstOrDefaultAsync();
if (payout is null)
return null;
return payout;
}
public static PullPaymentBlob GetBlob(this PullPaymentData data)
{
return JsonConvert.DeserializeObject<PullPaymentBlob>(Encoding.UTF8.GetString(data.Blob));
}
public static void SetBlob(this PullPaymentData data, PullPaymentBlob blob)
{
data.Blob = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(blob));
}
public static PaymentMethodId GetPaymentMethodId(this PayoutData data)
{
return PaymentMethodId.Parse(data.PaymentMethodId);
}
public static PayoutBlob GetBlob(this PayoutData data, BTCPayNetworkJsonSerializerSettings serializers)
{
return JsonConvert.DeserializeObject<PayoutBlob>(Encoding.UTF8.GetString(data.Blob), serializers.GetSerializer(data.GetPaymentMethodId().CryptoCode));
}
public static void SetBlob(this PayoutData data, PayoutBlob blob, BTCPayNetworkJsonSerializerSettings serializers)
{
data.Blob = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(blob, serializers.GetSerializer(data.GetPaymentMethodId().CryptoCode)));
}
public static bool IsSupported(this PullPaymentData data, BTCPayServer.Payments.PaymentMethodId paymentId)
{
return data.GetBlob().SupportedPaymentMethods.Contains(paymentId);
}
public static PayoutTransactionOnChainBlob GetProofBlob(this PayoutData data, BTCPayNetworkJsonSerializerSettings serializers)
{
if (data.Proof is null)
return null;
return JsonConvert.DeserializeObject<PayoutTransactionOnChainBlob>(Encoding.UTF8.GetString(data.Proof), serializers.GetSerializer(data.GetPaymentMethodId().CryptoCode));
}
public static void SetProofBlob(this PayoutData data, PayoutTransactionOnChainBlob blob, BTCPayNetworkJsonSerializerSettings serializers)
{
var bytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(blob, serializers.GetSerializer(data.GetPaymentMethodId().CryptoCode)));
// We only update the property if the bytes actually changed, this prevent from hammering the DB too much
if (data.Proof is null || bytes.Length != data.Proof.Length || !bytes.SequenceEqual(data.Proof))
{
data.Proof = bytes;
}
}
}
public class PayoutTransactionOnChainBlob
{
[JsonConverter(typeof(NBitcoin.JsonConverters.UInt256JsonConverter))]
public uint256 TransactionId { get; set; }
[JsonProperty(ItemConverterType = typeof(NBitcoin.JsonConverters.UInt256JsonConverter), NullValueHandling = NullValueHandling.Ignore)]
public HashSet<uint256> Candidates { get; set; } = new HashSet<uint256>();
}
public interface IClaimDestination
{
BitcoinAddress Address { get; }
}
public static class ClaimDestination
{
public static bool TryParse(string destination, BTCPayNetwork network, out IClaimDestination claimDestination)
{
if (destination == null)
throw new ArgumentNullException(nameof(destination));
destination = destination.Trim();
try
{
if (destination.StartsWith($"{network.UriScheme}:", StringComparison.OrdinalIgnoreCase))
{
claimDestination = new UriClaimDestination(new BitcoinUrlBuilder(destination, network.NBitcoinNetwork));
}
else
{
claimDestination = new AddressClaimDestination(BitcoinAddress.Create(destination, network.NBitcoinNetwork));
}
return true;
}
catch
{
claimDestination = null;
return false;
}
}
}
public class AddressClaimDestination : IClaimDestination
{
private readonly BitcoinAddress _bitcoinAddress;
public AddressClaimDestination(BitcoinAddress bitcoinAddress)
{
if (bitcoinAddress == null)
throw new ArgumentNullException(nameof(bitcoinAddress));
_bitcoinAddress = bitcoinAddress;
}
public BitcoinAddress BitcoinAdress => _bitcoinAddress;
public BitcoinAddress Address => _bitcoinAddress;
public override string ToString()
{
return _bitcoinAddress.ToString();
}
}
public class UriClaimDestination : IClaimDestination
{
private readonly BitcoinUrlBuilder _bitcoinUrl;
public UriClaimDestination(BitcoinUrlBuilder bitcoinUrl)
{
if (bitcoinUrl == null)
throw new ArgumentNullException(nameof(bitcoinUrl));
if (bitcoinUrl.Address is null)
throw new ArgumentException(nameof(bitcoinUrl));
_bitcoinUrl = bitcoinUrl;
}
public BitcoinUrlBuilder BitcoinUrl => _bitcoinUrl;
public BitcoinAddress Address => _bitcoinUrl.Address;
public override string ToString()
{
return _bitcoinUrl.ToString();
}
}
public class PayoutBlob
{
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal Amount { get; set; }
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal? CryptoAmount { get; set; }
public int MinimumConfirmation { get; set; } = 1;
public IClaimDestination Destination { get; set; }
public int Revision { get; set; }
}
public class ClaimDestinationJsonConverter : JsonConverter<IClaimDestination>
{
private readonly BTCPayNetwork _network;
public ClaimDestinationJsonConverter(BTCPayNetwork network)
{
if (network == null)
throw new ArgumentNullException(nameof(network));
_network = network;
}
public override IClaimDestination ReadJson(JsonReader reader, Type objectType, IClaimDestination existingValue, bool hasExistingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
return null;
if (reader.TokenType != JsonToken.String)
throw new JsonObjectException("Expected string for IClaimDestination", reader);
if (ClaimDestination.TryParse((string)reader.Value, _network, out var v))
return v;
throw new JsonObjectException("Invalid IClaimDestination", reader);
}
public override void WriteJson(JsonWriter writer, IClaimDestination value, JsonSerializer serializer)
{
if (value is IClaimDestination v)
writer.WriteValue(v.ToString());
}
}
public class PullPaymentBlob
{
public string Name { get; set; }
public string Currency { get; set; }
public int Divisibility { get; set; }
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal Limit { get; set; }
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal MinimumClaim { get; set; }
public PullPaymentView View { get; set; } = new PullPaymentView();
[JsonConverter(typeof(TimeSpanJsonConverter.Seconds))]
public TimeSpan? Period { get; set; }
[JsonProperty(ItemConverterType = typeof(PaymentMethodIdJsonConverter))]
public PaymentMethodId[] SupportedPaymentMethods { get; set; }
}
public class PullPaymentView
{
public string Title { get; set; }
public string Description { get; set; }
public string EmbeddedCSS { get; set; }
public string Email { get; set; }
public string CustomCSSLink { get; set; }
}
}

@ -7,7 +7,7 @@ using BTCPayServer.Client.JsonConverters;
using BTCPayServer.Client.Models;
using BTCPayServer.JsonConverters;
using BTCPayServer.Payments;
using BTCPayServer.Plugins.CoinSwitch;
using BTCPayServer.Payments.CoinSwitch;
using BTCPayServer.Rating;
using BTCPayServer.Services.Mails;
using BTCPayServer.Services.Rates;
@ -90,6 +90,8 @@ namespace BTCPayServer.Data
public bool AnyoneCanInvoice { get; set; }
public CoinSwitchSettings CoinSwitchSettings { get; set; }
string _LightningDescriptionTemplate;
public string LightningDescriptionTemplate
{

@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using BTCPayServer.Payments;
using NBitcoin;
using NBitcoin.DataEncoders;
@ -99,10 +98,6 @@ namespace BTCPayServer
JObject jobj = null;
try
{
if (HexEncoder.IsWellFormed(fileContents))
{
fileContents = Encoding.UTF8.GetString(Encoders.Hex.DecodeData(fileContents));
}
jobj = JObject.Parse(fileContents);
}
catch

@ -111,13 +111,6 @@ namespace BTCPayServer
s.Act = (o) => subscription(s, (T)o);
return Subscribe(eventType, s);
}
public IEventAggregatorSubscription Subscribe(Type eventType, Action<IEventAggregatorSubscription, object> subscription)
{
var s = new Subscription(this, eventType);
s.Act = (o) => subscription(s, o);
return Subscribe(eventType, s);
}
private IEventAggregatorSubscription Subscribe(Type eventType, Subscription subscription)
{

@ -1,4 +1,4 @@
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Invoices;
namespace BTCPayServer.Events
{

@ -1,24 +0,0 @@
using BTCPayServer.Payments;
using BTCPayServer.Services.Invoices;
namespace BTCPayServer.Events
{
public class InvoicePaymentMethodActivated : IHasInvoiceId
{
public PaymentMethodId PaymentMethodId { get; }
public InvoiceEntity InvoiceEntity { get; }
public InvoicePaymentMethodActivated(PaymentMethodId paymentMethodId, InvoiceEntity invoiceEntity)
{
PaymentMethodId = paymentMethodId;
InvoiceEntity = invoiceEntity;
}
public string InvoiceId => InvoiceEntity.Id;
public override string ToString()
{
return string.Empty;
}
}
}

@ -133,9 +133,9 @@ namespace BTCPayServer
finally { try { webSocket.Dispose(); } catch { } }
}
public static IEnumerable<BitcoinLikePaymentData> GetAllBitcoinPaymentData(this InvoiceEntity invoice, bool accountedOnly)
public static IEnumerable<BitcoinLikePaymentData> GetAllBitcoinPaymentData(this InvoiceEntity invoice)
{
return invoice.GetPayments(accountedOnly)
return invoice.GetPayments()
.Where(p => p.GetPaymentMethodId()?.PaymentType == PaymentTypes.BTCLike)
.Select(p => (BitcoinLikePaymentData)p.GetCryptoPaymentData())
.Where(data => data != null);

@ -1,103 +0,0 @@
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Data;
using BTCPayServer.Fido2.Models;
using BTCPayServer.Models;
using Fido2NetLib;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Fido2
{
[Route("fido2")]
[Authorize]
public class Fido2Controller : Controller
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly Fido2Service _fido2Service;
public Fido2Controller(UserManager<ApplicationUser> userManager, Fido2Service fido2Service)
{
_userManager = userManager;
_fido2Service = fido2Service;
}
[HttpGet("")]
public async Task<IActionResult> List()
{
return View(new Fido2AuthenticationViewModel()
{
Credentials = await _fido2Service.GetCredentials( _userManager.GetUserId(User))
});
}
[HttpGet("{id}/delete")]
public IActionResult Remove(string id)
{
return View("Confirm", new ConfirmModel("Are you sure you want to remove FIDO2 credential?", "Your account will no longer have this credential as an option for MFA.", "Remove"));
}
[HttpPost("{id}/delete")]
public async Task<IActionResult> RemoveP(string id)
{
await _fido2Service.Remove(id, _userManager.GetUserId(User));
TempData.SetStatusMessageModel(new StatusMessageModel
{
Severity = StatusMessageModel.StatusSeverity.Success,
Html = $"FIDO2 Credentials were removed successfully."
});
return RedirectToAction(nameof(List));
}
[HttpGet("register")]
public async Task<IActionResult> Create(AddFido2CredentialViewModel viewModel)
{
var options = await _fido2Service.RequestCreation(_userManager.GetUserId(User));
if (options is null)
{
TempData.SetStatusMessageModel(new StatusMessageModel
{
Severity = StatusMessageModel.StatusSeverity.Error,
Html = $"FIDO2 Credentials could not be saved."
});
return RedirectToAction(nameof(List));
}
ViewData["CredentialName"] = viewModel.Name ?? "";
return View(options);
}
[HttpPost("register")]
public async Task<IActionResult> CreateResponse([FromForm] string data, [FromForm] string name)
{
if (await _fido2Service.CompleteCreation(_userManager.GetUserId(User), name, data))
{
TempData.SetStatusMessageModel(new StatusMessageModel
{
Severity = StatusMessageModel.StatusSeverity.Success,
Html = $"FIDO2 Credentials were saved successfully."
});
}
else
{
TempData.SetStatusMessageModel(new StatusMessageModel
{
Severity = StatusMessageModel.StatusSeverity.Error,
Html = $"FIDO2 Credentials could not be saved."
});
}
return RedirectToAction(nameof(List));
}
}
}

@ -1,218 +0,0 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Fido2.Models;
using ExchangeSharp;
using Fido2NetLib;
using Fido2NetLib.Objects;
using Microsoft.EntityFrameworkCore;
using NBitcoin;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Fido2
{
public class Fido2Service
{
private static readonly ConcurrentDictionary<string, CredentialCreateOptions> CreationStore =
new ConcurrentDictionary<string, CredentialCreateOptions>();
private static readonly ConcurrentDictionary<string, AssertionOptions> LoginStore =
new ConcurrentDictionary<string, AssertionOptions>();
private readonly ApplicationDbContextFactory _contextFactory;
private readonly IFido2 _fido2;
private readonly Fido2Configuration _fido2Configuration;
public Fido2Service(ApplicationDbContextFactory contextFactory, IFido2 fido2, Fido2Configuration fido2Configuration)
{
_contextFactory = contextFactory;
_fido2 = fido2;
_fido2Configuration = fido2Configuration;
}
public async Task<CredentialCreateOptions> RequestCreation(string userId)
{
await using var dbContext = _contextFactory.CreateContext();
var user = await dbContext.Users.Include(applicationUser => applicationUser.Fido2Credentials)
.FirstOrDefaultAsync(applicationUser => applicationUser.Id == userId);
if (user == null)
{
return null;
}
// 2. Get user existing keys by username
var existingKeys =
user.Fido2Credentials
.Where(credential => credential.Type == Fido2Credential.CredentialType.FIDO2)
.Select(c => c.GetBlob().Descriptor).ToList();
// 3. Create options
var authenticatorSelection = new AuthenticatorSelection
{
RequireResidentKey = false, UserVerification = UserVerificationRequirement.Preferred
};
var exts = new AuthenticationExtensionsClientInputs()
{
Extensions = true,
UserVerificationIndex = true,
Location = true,
UserVerificationMethod = true,
BiometricAuthenticatorPerformanceBounds = new AuthenticatorBiometricPerfBounds
{
FAR = float.MaxValue, FRR = float.MaxValue
},
};
var options = _fido2.RequestNewCredential(
new Fido2User() {DisplayName = user.UserName, Name = user.UserName, Id = user.Id.ToBytesUTF8()},
existingKeys, authenticatorSelection, AttestationConveyancePreference.None, exts);
// options.Rp = new PublicKeyCredentialRpEntity(Request.Host.Host, options.Rp.Name, "");
CreationStore.AddOrReplace(userId, options);
return options;
}
public async Task<bool> CompleteCreation(string userId, string name, string data)
{
try
{
var attestationResponse = JObject.Parse(data).ToObject<AuthenticatorAttestationRawResponse>();
await using var dbContext = _contextFactory.CreateContext();
var user = await dbContext.Users.Include(applicationUser => applicationUser.Fido2Credentials)
.FirstOrDefaultAsync(applicationUser => applicationUser.Id == userId);
if (user == null || !CreationStore.TryGetValue(userId, out var options))
{
return false;
}
// 2. Verify and make the credentials
var success =
await _fido2.MakeNewCredentialAsync(attestationResponse, options, args => Task.FromResult(true));
// 3. Store the credentials in db
var newCredential = new Fido2Credential() {Name = name, ApplicationUserId = userId};
newCredential.SetBlob(new Fido2CredentialBlob()
{
Descriptor = new PublicKeyCredentialDescriptor(success.Result.CredentialId),
PublicKey = success.Result.PublicKey,
UserHandle = success.Result.User.Id,
SignatureCounter = success.Result.Counter,
CredType = success.Result.CredType,
AaGuid = success.Result.Aaguid.ToString(),
});
await dbContext.Fido2Credentials.AddAsync(newCredential);
await dbContext.SaveChangesAsync();
CreationStore.Remove(userId, out _);
return true;
}
catch (Exception)
{
return false;
}
}
public async Task<List<Fido2Credential>> GetCredentials(string userId)
{
await using var context = _contextFactory.CreateContext();
return await context.Fido2Credentials
.Where(device => device.ApplicationUserId == userId)
.ToListAsync();
}
public async Task Remove(string id, string userId)
{
await using var context = _contextFactory.CreateContext();
var device = await context.Fido2Credentials.FindAsync( id);
if (device == null || !device.ApplicationUserId.Equals(userId, StringComparison.InvariantCulture))
{
return;
}
context.Fido2Credentials.Remove(device);
await context.SaveChangesAsync();
}
public async Task<bool> HasCredentials(string userId)
{
await using var context = _contextFactory.CreateContext();
return await context.Fido2Credentials.Where(fDevice => fDevice.ApplicationUserId == userId).AnyAsync();
}
public async Task<AssertionOptions> RequestLogin(string userId)
{
await using var dbContext = _contextFactory.CreateContext();
var user = await dbContext.Users.Include(applicationUser => applicationUser.Fido2Credentials)
.FirstOrDefaultAsync(applicationUser => applicationUser.Id == userId);
if (!(user?.Fido2Credentials?.Any() is true))
{
return null;
}
var existingCredentials = user.Fido2Credentials
.Where(credential => credential.Type == Fido2Credential.CredentialType.FIDO2)
.Select(c => c.GetBlob().Descriptor)
.ToList();
var exts = new AuthenticationExtensionsClientInputs()
{
SimpleTransactionAuthorization = "FIDO",
GenericTransactionAuthorization = new TxAuthGenericArg
{
ContentType = "text/plain",
Content = new byte[] { 0x46, 0x49, 0x44, 0x4F }
},
UserVerificationIndex = true,
Location = true,
UserVerificationMethod = true ,
Extensions = true,
AppID = _fido2Configuration.Origin
};
// 3. Create options
var options = _fido2.GetAssertionOptions(
existingCredentials,
UserVerificationRequirement.Discouraged,
exts
);
LoginStore.AddOrReplace(userId, options);
return options;
}
public async Task<bool> CompleteLogin(string userId, AuthenticatorAssertionRawResponse response){
await using var dbContext = _contextFactory.CreateContext();
var user = await dbContext.Users.Include(applicationUser => applicationUser.Fido2Credentials)
.FirstOrDefaultAsync(applicationUser => applicationUser.Id == userId);
if (user == null || !LoginStore.TryGetValue(userId, out var options))
{
return false;
}
var credential = user.Fido2Credentials
.Where(fido2Credential => fido2Credential.Type is Fido2Credential.CredentialType.FIDO2)
.Select(fido2Credential => (fido2Credential, fido2Credential.GetBlob()))
.FirstOrDefault(fido2Credential => fido2Credential.Item2.Descriptor.Id.SequenceEqual(response.Id));
if (credential.Item2 is null)
{
return false;
}
// 5. Make the assertion
var res = await _fido2.MakeAssertionAsync(response, options, credential.Item2.PublicKey,
credential.Item2.SignatureCounter, x => Task.FromResult(true));
// 6. Store the updated counter
credential.Item2.SignatureCounter = res.Counter;
credential.fido2Credential.SetBlob(credential.Item2);
await dbContext.SaveChangesAsync();
LoginStore.Remove(userId, out _);
// 7. return OK to client
return true;
}
}
}

@ -1,31 +0,0 @@
using BTCPayServer.Data;
using BTCPayServer.Fido2.Models;
using NBXplorer;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Fido2
{
public static class Fido2Extensions
{
public static Fido2CredentialBlob GetBlob(this Fido2Credential credential)
{
var result = credential.Blob == null
? new Fido2CredentialBlob()
: JObject.Parse(ZipUtils.Unzip(credential.Blob)).ToObject<Fido2CredentialBlob>();
return result;
}
public static bool SetBlob(this Fido2Credential credential, Fido2CredentialBlob descriptor)
{
var original = new Serializer(null).ToString(credential.GetBlob());
var newBlob = new Serializer(null).ToString(descriptor);
if (original == newBlob)
return false;
credential.Type = Fido2Credential.CredentialType.FIDO2;
credential.Blob = ZipUtils.Zip(newBlob);
return true;
}
}
}

@ -1,11 +0,0 @@
using Fido2NetLib.Objects;
namespace BTCPayServer.Fido2.Models
{
public class AddFido2CredentialViewModel
{
public AuthenticatorAttachment? AuthenticatorAttachment { get; set; }
public string Name { get; set; }
}
}

@ -1,10 +0,0 @@
using System.Collections.Generic;
using BTCPayServer.Data;
namespace BTCPayServer.Fido2.Models
{
public class Fido2AuthenticationViewModel
{
public List<Fido2Credential> Credentials { get; set; }
}
}

@ -1,18 +0,0 @@
using Fido2NetLib;
using Fido2NetLib.Objects;
using Newtonsoft.Json;
namespace BTCPayServer.Fido2.Models
{
public class Fido2CredentialBlob
{
public PublicKeyCredentialDescriptor Descriptor { get; set; }
[JsonConverter(typeof(Base64UrlConverter))]
public byte[] PublicKey { get; set; }
[JsonConverter(typeof(Base64UrlConverter))]
public byte[] UserHandle { get; set; }
public uint SignatureCounter { get; set; }
public string CredType { get; set; }
public string AaGuid { get; set; }
}
}

@ -1,13 +0,0 @@
using Fido2NetLib;
namespace BTCPayServer.Fido2.Models
{
public class LoginWithFido2ViewModel
{
public string UserId { get; set; }
public bool RememberMe { get; set; }
public AssertionOptions Data { get; set; }
public string Response { get; set; }
}
}

@ -101,7 +101,7 @@ namespace BTCPayServer.HostedServices
}
}
if (accounting.Paid < accounting.MinimumTotalDue && invoice.GetPayments(true).Count != 0 && invoice.ExceptionStatus != InvoiceExceptionStatus.PaidPartial)
if (accounting.Paid < accounting.MinimumTotalDue && invoice.GetPayments().Count != 0 && invoice.ExceptionStatus != InvoiceExceptionStatus.PaidPartial)
{
invoice.ExceptionStatus = InvoiceExceptionStatus.PaidPartial;
context.MarkDirty();
@ -335,7 +335,7 @@ namespace BTCPayServer.HostedServices
{
bool extendInvoiceMonitoring = false;
var updateConfirmationCountIfNeeded = invoice
.GetPayments(false)
.GetPayments()
.Select<PaymentEntity, Task<PaymentEntity>>(async payment =>
{
var paymentData = payment.GetCryptoPaymentData();

@ -4,19 +4,20 @@ using System.Linq;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
using BTCPayServer;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.Logging;
using BTCPayServer.Payments;
using BTCPayServer.Services;
using BTCPayServer.Services.Notifications;
using BTCPayServer.Services.Notifications.Blobs;
using BTCPayServer.Services.Rates;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using NBitcoin;
using NBitcoin.DataEncoders;
using NBXplorer;
using PayoutData = BTCPayServer.Data.PayoutData;
using NBitcoin.RPC;
namespace BTCPayServer.HostedServices
{
@ -107,7 +108,7 @@ namespace BTCPayServer.HostedServices
Limit = create.Amount,
Period = o.Period is long periodSeconds ? (TimeSpan?)TimeSpan.FromSeconds(periodSeconds) : null,
SupportedPaymentMethods = create.PaymentMethodIds,
View = new PullPaymentBlob.PullPaymentView()
View = new PullPaymentView()
{
Title = create.Name ?? string.Empty,
Description = string.Empty,
@ -145,19 +146,19 @@ namespace BTCPayServer.HostedServices
BTCPayNetworkJsonSerializerSettings jsonSerializerSettings,
CurrencyNameTable currencyNameTable,
EventAggregator eventAggregator,
ExplorerClientProvider explorerClientProvider,
BTCPayNetworkProvider networkProvider,
NotificationSender notificationSender,
RateFetcher rateFetcher,
IEnumerable<IPayoutHandler> payoutHandlers)
RateFetcher rateFetcher)
{
_dbContextFactory = dbContextFactory;
_jsonSerializerSettings = jsonSerializerSettings;
_currencyNameTable = currencyNameTable;
_eventAggregator = eventAggregator;
_explorerClientProvider = explorerClientProvider;
_networkProvider = networkProvider;
_notificationSender = notificationSender;
_rateFetcher = rateFetcher;
_payoutHandlers = payoutHandlers;
}
Channel<object> _Channel;
@ -165,30 +166,19 @@ namespace BTCPayServer.HostedServices
private readonly BTCPayNetworkJsonSerializerSettings _jsonSerializerSettings;
private readonly CurrencyNameTable _currencyNameTable;
private readonly EventAggregator _eventAggregator;
private readonly ExplorerClientProvider _explorerClientProvider;
private readonly BTCPayNetworkProvider _networkProvider;
private readonly NotificationSender _notificationSender;
private readonly RateFetcher _rateFetcher;
private readonly IEnumerable<IPayoutHandler> _payoutHandlers;
private readonly CompositeDisposable _subscriptions = new CompositeDisposable();
internal override Task[] InitializeTasks()
{
_Channel = Channel.CreateUnbounded<object>();
foreach (IPayoutHandler payoutHandler in _payoutHandlers)
{
payoutHandler.StartBackgroundCheck(Subscribe);
}
_eventAggregator.Subscribe<NewOnChainTransactionEvent>(o => _Channel.Writer.TryWrite(o));
_eventAggregator.Subscribe<NewBlockEvent>(o => _Channel.Writer.TryWrite(o));
return new[] { Loop() };
}
private void Subscribe(params Type[] events)
{
foreach (Type @event in events)
{
_eventAggregator.Subscribe(@event, (subscription, o) => _Channel.Writer.TryWrite(o));
}
}
private async Task Loop()
{
await foreach (var o in _Channel.Reader.ReadAllAsync())
@ -202,13 +192,18 @@ namespace BTCPayServer.HostedServices
{
await HandleApproval(approv);
}
if (o is NewOnChainTransactionEvent newTransaction)
{
await UpdatePayoutsAwaitingForPayment(newTransaction);
}
if (o is CancelRequest cancel)
{
await HandleCancel(cancel);
}
foreach (IPayoutHandler payoutHandler in _payoutHandlers)
}
if (o is NewBlockEvent || o is NewOnChainTransactionEvent)
{
await payoutHandler.BackgroundCheck(o);
await UpdatePayoutsInProgress();
}
}
}
@ -271,18 +266,15 @@ namespace BTCPayServer.HostedServices
var paymentMethod = PaymentMethodId.Parse(payout.PaymentMethodId);
if (paymentMethod.CryptoCode == payout.PullPaymentData.GetBlob().Currency)
req.Rate = 1.0m;
var cryptoAmount = payoutBlob.Amount / req.Rate;
var payoutHandler = _payoutHandlers.First(handler => handler.CanHandle(paymentMethod));
var dest = await payoutHandler.ParseClaimDestination(paymentMethod, payoutBlob.Destination);
decimal minimumCryptoAmount = await payoutHandler.GetMinimumPayoutAmount(paymentMethod, dest);
if (cryptoAmount < minimumCryptoAmount)
var cryptoAmount = Money.Coins(payoutBlob.Amount / req.Rate);
Money mininumCryptoAmount = GetMinimumCryptoAmount(paymentMethod, payoutBlob.Destination.Address.ScriptPubKey);
if (cryptoAmount < mininumCryptoAmount)
{
req.Completion.TrySetResult(PayoutApproval.Result.TooLowAmount);
return;
}
payoutBlob.CryptoAmount = BTCPayServer.Extensions.RoundUp(cryptoAmount, _networkProvider.GetNetwork(paymentMethod.CryptoCode).Divisibility);
payout.SetBlob(payoutBlob, _jsonSerializerSettings);
payoutBlob.CryptoAmount = cryptoAmount.ToDecimal(MoneyUnit.BTC);
payout.SetBlob(payoutBlob, this._jsonSerializerSettings);
await ctx.SaveChangesAsync();
req.Completion.SetResult(PayoutApproval.Result.Ok);
}
@ -297,9 +289,8 @@ namespace BTCPayServer.HostedServices
try
{
DateTimeOffset now = DateTimeOffset.UtcNow;
await using var ctx = _dbContextFactory.CreateContext();
using var ctx = _dbContextFactory.CreateContext();
var pp = await ctx.PullPayments.FindAsync(req.ClaimRequest.PullPaymentId);
if (pp is null || pp.Archived)
{
req.Completion.TrySetResult(new ClaimRequest.ClaimResponse(ClaimRequest.ClaimResult.Archived));
@ -316,9 +307,7 @@ namespace BTCPayServer.HostedServices
return;
}
var ppBlob = pp.GetBlob();
var payoutHandler =
_payoutHandlers.FirstOrDefault(handler => handler.CanHandle(req.ClaimRequest.PaymentMethodId));
if (!ppBlob.SupportedPaymentMethods.Contains(req.ClaimRequest.PaymentMethodId) || payoutHandler is null)
if (!ppBlob.SupportedPaymentMethods.Contains(req.ClaimRequest.PaymentMethodId))
{
req.Completion.TrySetResult(new ClaimRequest.ClaimResponse(ClaimRequest.ClaimResult.PaymentMethodNotSupported));
return;
@ -347,7 +336,7 @@ namespace BTCPayServer.HostedServices
State = PayoutState.AwaitingApproval,
PullPaymentDataId = req.ClaimRequest.PullPaymentId,
PaymentMethodId = req.ClaimRequest.PaymentMethodId.ToString(),
Destination = req.ClaimRequest.Destination.ToString()
Destination = GetDestination(req.ClaimRequest.Destination.Address.ScriptPubKey)
};
if (claimed < ppBlob.MinimumClaim || claimed == 0.0m)
{
@ -357,10 +346,11 @@ namespace BTCPayServer.HostedServices
var payoutBlob = new PayoutBlob()
{
Amount = claimed,
Destination = req.ClaimRequest.Destination.ToString()
Destination = req.ClaimRequest.Destination
};
payout.SetBlob(payoutBlob, _jsonSerializerSettings);
await ctx.Payouts.AddAsync(payout);
payout.SetProofBlob(new PayoutTransactionOnChainBlob(), _jsonSerializerSettings);
ctx.Payouts.Add(payout);
try
{
await ctx.SaveChangesAsync();
@ -383,6 +373,54 @@ namespace BTCPayServer.HostedServices
req.Completion.TrySetException(ex);
}
}
private async Task UpdatePayoutsAwaitingForPayment(NewOnChainTransactionEvent newTransaction)
{
try
{
var outputs = newTransaction.
NewTransactionEvent.
TransactionData.
Transaction.
Outputs;
var destinations = outputs.Select(o => GetDestination(o.ScriptPubKey)).ToHashSet();
using var ctx = _dbContextFactory.CreateContext();
var payouts = await ctx.Payouts
.Include(o => o.PullPaymentData)
.Where(p => p.State == PayoutState.AwaitingPayment)
.Where(p => destinations.Contains(p.Destination))
.ToListAsync();
var payoutByDestination = payouts.ToDictionary(p => p.Destination);
foreach (var output in outputs)
{
if (!payoutByDestination.TryGetValue(GetDestination(output.ScriptPubKey), out var payout))
continue;
var payoutBlob = payout.GetBlob(this._jsonSerializerSettings);
if (output.Value.ToDecimal(MoneyUnit.BTC) != payoutBlob.CryptoAmount)
continue;
var proof = payout.GetProofBlob(this._jsonSerializerSettings);
var txId = newTransaction.NewTransactionEvent.TransactionData.TransactionHash;
if (proof.Candidates.Add(txId))
{
payout.State = PayoutState.InProgress;
if (proof.TransactionId is null)
proof.TransactionId = txId;
payout.SetProofBlob(proof, _jsonSerializerSettings);
var walletId = new WalletId(payout.PullPaymentData.StoreId, newTransaction.CryptoCode);
_eventAggregator.Publish(new UpdateTransactionLabel(walletId,
newTransaction.NewTransactionEvent.TransactionData.TransactionHash,
UpdateTransactionLabel.PayoutTemplate(payout.Id,payout.PullPaymentDataId, walletId.ToString())));
}
}
await ctx.SaveChangesAsync();
}
catch (Exception ex)
{
Logs.PayServer.LogWarning(ex, "Error while processing a transaction in the pull payment hosted service");
}
}
private async Task HandleCancel(CancelRequest cancel)
{
try
@ -419,6 +457,95 @@ namespace BTCPayServer.HostedServices
cancel.Completion.TrySetException(ex);
}
}
private async Task UpdatePayoutsInProgress()
{
try
{
using var ctx = _dbContextFactory.CreateContext();
var payouts = await ctx.Payouts
.Include(p => p.PullPaymentData)
.Where(p => p.State == PayoutState.InProgress)
.ToListAsync();
foreach (var payout in payouts)
{
var proof = payout.GetProofBlob(this._jsonSerializerSettings);
var payoutBlob = payout.GetBlob(this._jsonSerializerSettings);
foreach (var txid in proof.Candidates.ToList())
{
var explorer = _explorerClientProvider.GetExplorerClient(payout.GetPaymentMethodId().CryptoCode);
var tx = await explorer.GetTransactionAsync(txid);
if (tx is null)
{
proof.Candidates.Remove(txid);
}
else if (tx.Confirmations >= payoutBlob.MinimumConfirmation)
{
payout.State = PayoutState.Completed;
proof.TransactionId = tx.TransactionHash;
payout.Destination = null;
break;
}
else
{
var rebroadcasted = await explorer.BroadcastAsync(tx.Transaction);
if (rebroadcasted.RPCCode == RPCErrorCode.RPC_TRANSACTION_ERROR ||
rebroadcasted.RPCCode == RPCErrorCode.RPC_TRANSACTION_REJECTED)
{
proof.Candidates.Remove(txid);
}
else
{
payout.State = PayoutState.InProgress;
proof.TransactionId = tx.TransactionHash;
continue;
}
}
}
if (proof.TransactionId is null && !proof.Candidates.Contains(proof.TransactionId))
{
proof.TransactionId = null;
}
if (proof.Candidates.Count == 0)
{
payout.State = PayoutState.AwaitingPayment;
}
else if (proof.TransactionId is null)
{
proof.TransactionId = proof.Candidates.First();
}
if (payout.State == PayoutState.Completed)
proof.Candidates = null;
payout.SetProofBlob(proof, this._jsonSerializerSettings);
}
await ctx.SaveChangesAsync();
}
catch (Exception ex)
{
Logs.PayServer.LogWarning(ex, "Error while processing an update in the pull payment hosted service");
}
}
private Money GetMinimumCryptoAmount(PaymentMethodId paymentMethodId, Script scriptPubKey)
{
Money mininumAmount = Money.Zero;
if (_networkProvider.GetNetwork<BTCPayNetwork>(paymentMethodId.CryptoCode)?
.NBitcoinNetwork?
.Consensus?
.ConsensusFactory?
.CreateTxOut() is TxOut txout)
{
txout.ScriptPubKey = scriptPubKey;
mininumAmount = txout.GetDustThreshold(new FeeRate(1.0m));
}
return mininumAmount;
}
private static string GetDestination(Script scriptPubKey)
{
return Encoders.Base64.EncodeData(scriptPubKey.ToBytes(true));
}
public Task Cancel(CancelRequest cancelRequest)
{
CancellationToken.ThrowIfCancellationRequested();
@ -441,7 +568,6 @@ namespace BTCPayServer.HostedServices
public override Task StopAsync(CancellationToken cancellationToken)
{
_Channel?.Writer.Complete();
_subscriptions.Dispose();
return base.StopAsync(cancellationToken);
}
}

@ -0,0 +1,30 @@
using System;
using System.Threading.Tasks;
using BTCPayServer.Configuration;
using BTCPayServer.Services;
namespace BTCPayServer.HostedServices
{
public class TorServicesHostedService : BaseAsyncService
{
private readonly BTCPayServerOptions _options;
private readonly TorServices _torServices;
public TorServicesHostedService(BTCPayServerOptions options, TorServices torServices)
{
_options = options;
_torServices = torServices;
}
internal override Task[] InitializeTasks()
{
return new Task[] { CreateLoopTask(RefreshTorServices) };
}
async Task RefreshTorServices()
{
await _torServices.Refresh();
await Task.Delay(TimeSpan.FromSeconds(120), Cancellation);
}
}
}

@ -46,7 +46,7 @@ namespace BTCPayServer.HostedServices
UpdateTransactionLabel.InvoiceLabelTemplate(invoiceEvent.Invoice.Id)
};
if (invoiceEvent.Invoice.GetPayments(invoiceEvent.Payment.GetCryptoCode(), false).Any(entity =>
if (invoiceEvent.Invoice.GetPayments(invoiceEvent.Payment.GetCryptoCode()).Any(entity =>
entity.GetCryptoPaymentData() is BitcoinLikePaymentData pData &&
pData.PayjoinInformation?.CoinjoinTransactionHash == transactionId))
{

@ -101,7 +101,7 @@ namespace BTCPayServer.HostedServices
webhookEvent.DeliveryId = newDelivery.Id;
webhookEvent.WebhookId = webhookDelivery.Webhook.Id;
// if we redelivered a redelivery, we still want the initial delivery here
webhookEvent.OriginalDeliveryId ??= deliveryId;
webhookEvent.OrignalDeliveryId ??= deliveryId;
webhookEvent.IsRedelivery = true;
newDeliveryBlob.Request = ToBytes(webhookEvent);
newDelivery.SetBlob(newDeliveryBlob);
@ -125,7 +125,7 @@ namespace BTCPayServer.HostedServices
webhookEvent.StoreId = invoiceEvent.Invoice.StoreId;
webhookEvent.DeliveryId = delivery.Id;
webhookEvent.WebhookId = webhook.Id;
webhookEvent.OriginalDeliveryId = delivery.Id;
webhookEvent.OrignalDeliveryId = delivery.Id;
webhookEvent.IsRedelivery = false;
webhookEvent.Timestamp = delivery.Timestamp;
var context = new WebhookDeliveryRequest(webhook.Id, webhookEvent, delivery, webhookBlob);

@ -35,6 +35,7 @@ using BTCPayServer.Services.PaymentRequests;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Wallets;
using BTCPayServer.U2F;
using BundlerMinifier.TagHelpers;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
@ -82,6 +83,7 @@ namespace BTCPayServer.Hosting
});
services.AddSingleton<BTCPayNetworkJsonSerializerSettings>();
services.RegisterJsonConverter(n => new ClaimDestinationJsonConverter(n));
services.AddPayJoinServices();
#if ALTCOINS
@ -92,7 +94,6 @@ namespace BTCPayServer.Hosting
services.TryAddSingleton<ISettingsRepository>(provider => provider.GetService<SettingsRepository>());
services.TryAddSingleton<LabelFactory>();
services.TryAddSingleton<TorServices>();
services.AddSingleton<IHostedService>(provider => provider.GetRequiredService<TorServices>());
services.TryAddSingleton<SocketFactory>();
services.TryAddSingleton<LightningClientFactoryService>();
services.TryAddSingleton<InvoicePaymentNotification>();
@ -112,6 +113,7 @@ namespace BTCPayServer.Hosting
services.TryAddSingleton<WalletRepository>();
services.TryAddSingleton<EventAggregator>();
services.TryAddSingleton<PaymentRequestService>();
services.TryAddSingleton<U2FService>();
services.AddSingleton<ApplicationDbContextFactory>();
services.AddOptions<BTCPayServerOptions>().Configure(
(options) =>
@ -316,8 +318,6 @@ namespace BTCPayServer.Hosting
.ConfigureHttpClient(h => h.DefaultRequestHeaders.ConnectionClose = true)
.ConfigurePrimaryHttpMessageHandler<Socks5HttpClientHandler>();
services.AddSingleton<IPayoutHandler, BitcoinLikePayoutHandler>();
services.AddSingleton<HostedServices.PullPaymentHostedService>();
services.AddSingleton<IHostedService, HostedServices.PullPaymentHostedService>(o => o.GetRequiredService<PullPaymentHostedService>());
@ -345,6 +345,7 @@ namespace BTCPayServer.Hosting
services.AddSingleton<IHostedService, TransactionLabelMarkerHostedService>();
services.AddSingleton<IHostedService, UserEventHostedService>();
services.AddSingleton<IHostedService, DynamicDnsHostedService>();
services.AddSingleton<IHostedService, TorServicesHostedService>();
services.AddSingleton<IHostedService, PaymentRequestStreamer>();
services.AddSingleton<IBackgroundJobClient, BackgroundJobClient>();
services.AddScoped<IAuthorizationHandler, CookieAuthorizationHandler>();

@ -1,5 +1,4 @@
using System;
using System.Globalization;
using System.Linq;
using System.Net.WebSockets;
using System.Threading.Tasks;
@ -33,8 +32,6 @@ namespace BTCPayServer.Hosting
public async Task Invoke(HttpContext httpContext)
{
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
CultureInfo.CurrentUICulture = CultureInfo.InvariantCulture;
try
{
var bitpayAuth = GetBitpayAuth(httpContext, out bool isBitpayAuth);

@ -1,27 +1,27 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Client.Models;
using BTCPayServer.Configuration;
using BTCPayServer.Data;
using BTCPayServer.Fido2;
using BTCPayServer.Fido2.Models;
using BTCPayServer.Logging;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Services;
using BTCPayServer.Services.Stores;
using Fido2NetLib.Objects;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NBitcoin.DataEncoders;
using NBXplorer;
using Newtonsoft.Json.Linq;
using PeterO.Cbor;
using Newtonsoft.Json.Serialization;
namespace BTCPayServer.Hosting
{
@ -121,13 +121,6 @@ namespace BTCPayServer.Hosting
settings.TransitionInternalNodeConnectionString = true;
await _Settings.UpdateSetting(settings);
}
if (!settings.MigrateU2FToFIDO2)
{
await MigrateU2FToFIDO2();
settings.MigrateU2FToFIDO2 = true;
await _Settings.UpdateSetting(settings);
}
}
catch (Exception ex)
{
@ -136,59 +129,6 @@ namespace BTCPayServer.Hosting
}
}
private async Task MigrateU2FToFIDO2()
{
await using var ctx = _DBContextFactory.CreateContext();
var u2fDevices = await ctx.U2FDevices.ToListAsync();
foreach (U2FDevice u2FDevice in u2fDevices)
{
var fido2 = new Fido2Credential()
{
ApplicationUserId = u2FDevice.ApplicationUserId,
Name = u2FDevice.Name,
Type = Fido2Credential.CredentialType.FIDO2
};
fido2.SetBlob(new Fido2CredentialBlob()
{
SignatureCounter = (uint)u2FDevice.Counter,
PublicKey = CreatePublicKeyFromU2fRegistrationData( u2FDevice.PublicKey).EncodeToBytes() ,
UserHandle = u2FDevice.KeyHandle,
Descriptor = new PublicKeyCredentialDescriptor(u2FDevice.KeyHandle),
CredType = "u2f"
});
await ctx.AddAsync(fido2);
ctx.Remove(u2FDevice);
}
await ctx.SaveChangesAsync();
}
//from https://github.com/abergs/fido2-net-lib/blob/0fa7bb4b4a1f33f46c5f7ca4ee489b47680d579b/Test/ExistingU2fRegistrationDataTests.cs#L70
private static CBORObject CreatePublicKeyFromU2fRegistrationData(byte[] publicKeyData)
{
if (publicKeyData.Length != 65)
{
throw new ArgumentException("u2f public key must be 65 bytes", nameof(publicKeyData));
}
var x = new byte[32];
var y = new byte[32];
Buffer.BlockCopy(publicKeyData, 1, x, 0, 32);
Buffer.BlockCopy(publicKeyData, 33, y, 0, 32);
var coseKey = CBORObject.NewMap();
coseKey.Add(COSE.KeyCommonParameter.KeyType, COSE.KeyType.EC2);
coseKey.Add(COSE.KeyCommonParameter.Alg, -7);
coseKey.Add(COSE.KeyTypeParameter.Crv, COSE.EllipticCurve.P256);
coseKey.Add(COSE.KeyTypeParameter.X, x);
coseKey.Add(COSE.KeyTypeParameter.Y, y);
return coseKey;
}
private async Task TransitionInternalNodeConnectionString()
{
var nodes = LightningOptions.Value.InternalLightningByCryptoCode.Values.Select(c => c.ToString()).ToHashSet();

@ -1,10 +1,8 @@
using System;
using System.IO;
using System.Linq;
using System.Net;
using BTCPayServer.Configuration;
using BTCPayServer.Data;
using BTCPayServer.Fido2;
using BTCPayServer.Filters;
using BTCPayServer.Logging;
using BTCPayServer.PaymentRequest;
@ -14,11 +12,9 @@ using BTCPayServer.Services.Apps;
using BTCPayServer.Storage;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Fido2NetLib;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Server.Kestrel.Core;
@ -30,9 +26,6 @@ using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Net.Http.Headers;
using NBitcoin;
using Microsoft.AspNetCore.Mvc;
using BTCPayServer.Controllers.GreenField;
using System.Globalization;
namespace BTCPayServer.Hosting
{
@ -85,36 +78,12 @@ namespace BTCPayServer.Hosting
services.AddProviderStorage();
services.AddSession();
services.AddSignalR();
services.AddFido2(options =>
{
options.ServerName = "BTCPay Server";
})
.AddCachedMetadataService(config =>
{
//They'll be used in a "first match wins" way in the order registered
config.AddStaticMetadataRepository();
});
var descriptor =services.Single(descriptor => descriptor.ServiceType == typeof(Fido2Configuration));
services.Remove(descriptor);
services.AddScoped(provider =>
{
var httpContext = provider.GetService<IHttpContextAccessor>();
return new Fido2Configuration()
{
ServerName = "BTCPay Server",
Origin = $"{httpContext.HttpContext.Request.Scheme}://{httpContext.HttpContext.Request.Host}",
ServerDomain = httpContext.HttpContext.Request.Host.Host
};
});
services.AddScoped<Fido2Service>();
var mvcBuilder= services.AddMvc(o =>
{
o.Filters.Add(new XFrameOptionsAttribute("DENY"));
o.Filters.Add(new XContentTypeOptionsAttribute("nosniff"));
o.Filters.Add(new XXSSProtectionAttribute());
o.Filters.Add(new ReferrerPolicyAttribute("same-origin"));
o.ModelBinderProviders.Insert(0, new ModelBinders.DefaultModelBinderProvider());
//o.Filters.Add(new ContentSecurityPolicyAttribute()
//{
// FontSrc = "'self' https://fonts.gstatic.com/",
@ -123,12 +92,14 @@ namespace BTCPayServer.Hosting
// StyleSrc = "'self' 'unsafe-inline'",
// ScriptSrc = "'self' 'unsafe-inline'"
//});
})
})
.ConfigureApiBehaviorOptions(options =>
{
var builtInFactory = options.InvalidModelStateResponseFactory;
options.InvalidModelStateResponseFactory = context =>
{
return new UnprocessableEntityObjectResult(context.ModelState.ToGreenfieldValidationError());
context.HttpContext.Response.StatusCode = (int)HttpStatusCode.UnprocessableEntity;
return builtInFactory(context);
};
})
.AddRazorOptions(o =>
@ -144,6 +115,8 @@ namespace BTCPayServer.Hosting
.AddPlugins(services, Configuration, LoggerFactory)
.AddControllersAsServices();
services.TryAddScoped<ContentSecurityPolicies>();
services.Configure<IdentityOptions>(options =>
{

@ -1,46 +0,0 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.VisualBasic.CompilerServices;
namespace BTCPayServer.ModelBinders
{
public class DateTimeOffsetModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (!typeof(DateTimeOffset).GetTypeInfo().IsAssignableFrom(bindingContext.ModelType) &&
!typeof(DateTimeOffset?).GetTypeInfo().IsAssignableFrom(bindingContext.ModelType))
{
return Task.CompletedTask;
}
ValueProviderResult val = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (val == null)
{
return Task.CompletedTask;
}
string v = val.FirstValue as string;
if (v == null)
{
return Task.CompletedTask;
}
try
{
var sec = long.Parse(v, CultureInfo.InvariantCulture);
bindingContext.Result = ModelBindingResult.Success(NBitcoin.Utils.UnixTimeToDateTime(sec));
}
catch
{
bindingContext.Result = ModelBindingResult.Failed();
bindingContext.ModelState.AddModelError(bindingContext.ModelName, "Invalid unix timestamp");
}
return Task.CompletedTask;
}
}
}

@ -1,24 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using BTCPayServer.Payments;
using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace BTCPayServer.ModelBinders
{
public class DefaultModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context.Metadata.ModelType == typeof(decimal) || context.Metadata.ModelType == typeof(decimal?))
return new InvariantDecimalModelBinder();
if (context.Metadata.ModelType == typeof(PaymentMethodId))
return new PaymentMethodIdModelBinder();
if (context.Metadata.ModelType == typeof(WalletIdModelBinder))
return new ModelBinders.WalletIdModelBinder();
return null;
}
}
}

@ -43,17 +43,11 @@ namespace BTCPayServer.ModelBinders
var data = network.NBXplorerNetwork.DerivationStrategyFactory.Parse(key);
if (!bindingContext.ModelType.IsInstanceOfType(data))
{
bindingContext.Result = ModelBindingResult.Failed();
bindingContext.ModelState.AddModelError(bindingContext.ModelName, "Invalid derivation scheme");
return Task.CompletedTask;
throw new FormatException("Invalid destination type");
}
bindingContext.Result = ModelBindingResult.Success(data);
}
catch
{
bindingContext.Result = ModelBindingResult.Failed();
bindingContext.ModelState.AddModelError(bindingContext.ModelName, "Invalid derivation scheme");
}
catch { throw new FormatException("Invalid derivation scheme"); }
return Task.CompletedTask;
}

@ -31,11 +31,6 @@ namespace BTCPayServer.ModelBinders
{
bindingContext.Result = ModelBindingResult.Success(paymentId);
}
else
{
bindingContext.Result = ModelBindingResult.Failed();
bindingContext.ModelState.AddModelError(bindingContext.ModelName, "Invalid payment id");
}
return Task.CompletedTask;
}
}

@ -30,11 +30,6 @@ namespace BTCPayServer.ModelBinders
{
bindingContext.Result = ModelBindingResult.Success(walletId);
}
else
{
bindingContext.Result = ModelBindingResult.Failed();
bindingContext.ModelState.AddModelError(bindingContext.ModelName, "Invalid wallet id");
}
return Task.CompletedTask;
}
}

@ -1,10 +1,10 @@
using BTCPayServer.Fido2.Models;
using BTCPayServer.U2F.Models;
namespace BTCPayServer.Models.AccountViewModels
{
public class SecondaryLoginViewModel
{
public LoginWithFido2ViewModel LoginWithFido2ViewModel { get; set; }
public LoginWith2faViewModel LoginWith2FaViewModel { get; set; }
public LoginWithU2FViewModel LoginWithU2FViewModel { get; set; }
}
}

@ -9,8 +9,6 @@ namespace BTCPayServer.Models.AppViewModels
public class UpdateCrowdfundViewModel
{
public string StoreId { get; set; }
public string StoreName { get; set; }
[Required]
[MaxLength(30)]
public string Title { get; set; }
@ -97,7 +95,12 @@ namespace BTCPayServer.Models.AppViewModels
[Display(Name = "Colors to rotate between with animation when a payment is made. First color is the default background. One color per line. Can be any valid css color value.")]
public string AnimationColors { get; set; }
// NOTE: Improve validation if needed
public bool ModelWithMinimumData => Description != null && Title != null && TargetCurrency != null;
public bool ModelWithMinimumData
{
get { return Description != null && Title != null && TargetCurrency != null; }
}
}
}

@ -9,8 +9,6 @@ namespace BTCPayServer.Models.AppViewModels
public class UpdatePointOfSaleViewModel
{
public string StoreId { get; set; }
public string StoreName { get; set; }
[Required]
[MaxLength(30)]
public string Title { get; set; }

Some files were not shown because too many files have changed in this diff Show More