Compare commits

..

62 Commits

Author SHA1 Message Date
8bd54493a3 add pgp verif 2022-05-09 14:00:21 +02:00
225243ba93 1.5.2 changelog (#3730)
* 1.5.2 chaneglog

* bump
2022-05-09 20:58:49 +09:00
21bd35accd Fix shopify id / order no (#3718)
fixes #3636
2022-05-09 20:58:05 +09:00
4acca4c6f5 Try fix selenium test error (#3728) 2022-05-09 14:13:51 +09:00
c106ff8290 bump selenium (#3723) 2022-05-06 12:07:53 +09:00
296ba4606d Redirect to payment method setup (#3721)
Closes #3651.
2022-05-06 11:14:56 +09:00
54dd602be4 Implementing tables fix per dennisreimann suggestion 2022-05-05 15:17:44 +02:00
9cac8d36ad A few tables in the Cart checkout were having bad width and margins which caused misalignments. 2022-05-05 15:17:44 +02:00
d407e9ffba Add dark mode option for public pull payment and payment request views
Closes #3706. Closes #3705.
2022-05-04 14:36:37 +02:00
26025b564e Lightning: Catch and display external service errors
Prevent crashes like in #3700 and display the error to the user.
2022-05-04 14:30:07 +02:00
4cacc2a9e6 bump LNURL package 2022-05-04 14:08:02 +02:00
16ef405670 bump LNURL package 2022-05-04 13:31:15 +02:00
642aad28ac Update validation of crowdfund app settings (#3708) 2022-05-04 10:34:40 +02:00
B
e5feda69c8 Updated Payout processor Label for setting interval (#3698) 2022-05-04 10:34:31 +02:00
aa89d1dffe Makle plugin packer store in dir with plugin name 2022-05-04 09:01:17 +02:00
662f269a94 Fix return type of webhooks get so that plugins can use it 2022-05-03 11:15:08 +02:00
6f80100ee6 Plugin Packer: Generate versioned dir and also SHA256SUMS (+ asc file is on windows with gpg and powershell installed) 2022-05-03 08:12:25 +02:00
38d3154ddf Dashboard: Prevent y-axis labels from getting cut off (#3702)
Fixes #3692.
2022-05-02 21:32:24 +09:00
e16a718bde Do not always provide counting in list views (#3696) 2022-05-02 16:35:28 +09:00
a40429e219 Fix possible NRE 2022-05-02 09:43:55 +09:00
e5602a86a2 bump 2022-04-30 12:58:58 +09:00
7d4d8bff93 Do not show balance if can't get the balance (#3695) 2022-04-30 12:56:05 +09:00
d028ebfdf1 Improve performance of Invoice count (#3688) 2022-04-30 12:54:44 +09:00
a5aeb2a3c1 Fix performance issue on dashboard for big wallets (#3694) 2022-04-30 12:54:12 +09:00
410bd5ab9c Remove logs about pending invoices 2022-04-29 16:01:25 +09:00
f4823d962a Do not crash if /apps/{appId} do not exists 2022-04-29 16:00:27 +09:00
9aa35f3488 add missing docs 2022-04-29 08:58:17 +02:00
d2a7e5dc4d get pgp verified commit 2022-04-28 16:05:33 +02:00
f7cb44c343 fix crash on not found perk items (#3685) 2022-04-28 16:03:19 +02:00
ae67cc8a51 bump 2022-04-28 22:06:03 +09:00
454d628f93 1.5.0 changelog (#3680)
* 1.5.0 changelog

* Apply suggestions from code review

Co-authored-by: d11n <mail@dennisreimann.de>

* Update Changelog.md

Co-authored-by: d11n <mail@dennisreimann.de>
2022-04-28 22:04:47 +09:00
e60e8dc9ba reduce payout processor logs 2022-04-28 14:43:01 +02:00
382fe5cd47 Auto approve refunds (#3682) 2022-04-28 20:50:28 +09:00
86e53552c0 Upgrade Lightning library (#3683) 2022-04-28 12:49:43 +02:00
6296d63197 Local API client: Allow for cancellation of GetPayment calls (#3681)
I missed adding those in #3674.
2022-04-28 09:51:21 +09:00
ed1a7bb887 Allow auto approval of claims for pull payments (#1851)
* Allow auto approval of claims for pull payments

closes #1780

* fix
2022-04-28 09:51:04 +09:00
273bc78db3 Allow Users to be disabled/enabled (#3639)
* Allow Users to be disabled/enabled

* rebrand to locked for api

* Update BTCPayServer/Views/UIAccount/Lockout.cshtml

Co-authored-by: d11n <mail@dennisreimann.de>

* fix docker compose and an uneeded check in api handler

* fix

* Add enabled user test

Co-authored-by: d11n <mail@dennisreimann.de>
Co-authored-by: Nicolas Dorier <nicolas.dorier@gmail.com>
2022-04-26 21:27:35 +09:00
261a3ecee3 Greenfield: Allow for cancellation of Lightning method calls (#3674) 2022-04-26 10:29:20 +09:00
8f0ac61634 Refactoring: Extract Safe module to Abstractions (#3676)
So that it can be used by plugins as well.
2022-04-26 10:28:49 +09:00
5a7b275ee3 Fix CanCreateAppPos 2022-04-25 17:37:46 +09:00
54e106ec74 LNURL: Use Lightning description template in LNURL metadata (#3667)
* LNURL: Use Lightning description template in LNURL metadata

Unifies the invoice description for the Lightning and LNURL payment methods and fixes #3634.

* Add POS Order ID for consistency with Crowdfund

* LNURL: Add POS item metadata to invoice

For consistency with the Lightning payment method. Closes #3655.
2022-04-24 20:36:10 +09:00
51690b47a3 Transfer Processors (#3476)
* Automated Transfer processors

This PR introduces a few things:
* Payouts can now be directly nested under a store instead of through a pull payment.
* The Wallet Send screen now has an option to "schedule" instead of simply creating a transaction. When you click on schedule, all transaction destinations are converted into approved payouts. Any options relating to fees or coin selection are discarded.
* There is a new concept introduced, called "Transfer Processors".  Transfer Processors are services for stores that process payouts that are awaiting payment. Each processor specifies which payment methods it can handle.  BTCPay Server will have some forms of transfer processors baked in but it has been designed to allow the Plugin System to provide additional processors.
* The initial transfer processors provided are "automated processors", for on chain and lightning payment methods. They can be configured to process payouts every X amount of minutes. For  on-chain, this means payments are batched into one transaction, resulting in more efficient and cheaper fees for processing.
*

* fix build

* extract

* remove magic string stuff

* fix error message when scheduling

* Paginate migration

* add payout count to payment method tab

* remove unused var

* add protip

* optimzie payout migration dramatically

* Remove useless double condition

* Fix bunch of warnings

* Remove warning

* Remove warnigns

* Rename to Payout processors

* fix typo

Co-authored-by: Nicolas Dorier <nicolas.dorier@gmail.com>
2022-04-24 12:19:34 +09:00
9ab129ba89 More URL id parameter fixes (#3673) 2022-04-22 15:52:57 +02:00
b39a67534c Greenfield: Proper response for store not found (#3658)
* Greenfield: Proper response for store not found

Previously the "network not configured" error message was also displayed when indeed the store could not be found. This is misleading, hence we should have a separate error for that case.

* Use StoreNotFound method
2022-04-22 12:17:20 +02:00
2d717cbf01 Fix payment request redirect URL (#3672) 2022-04-22 10:28:46 +02:00
c4f284aaf1 Lightning: Add GetPayment methods to local client (#3657)
I missed adding them in #3557.
2022-04-22 09:28:11 +09:00
e40a9089e0 Merge pull request #3666 from dennisreimann/ivpn
Add IVPN as supporter to README
2022-04-21 16:02:33 +02:00
8cf849e590 Add IVPN as supporter to README 2022-04-21 15:50:46 +02:00
03e113b063 Merge pull request #3665 from dennisreimann/ivpn
Add IVPN logo for supporters section
2022-04-21 15:36:38 +02:00
d9305b1e63 Add IVPN logo for supporters section
We are not displaying it in the app, but linking to it from various other sites (docs, website, etc.)
2022-04-21 15:34:09 +02:00
182fa7d803 Upgrade Lightning lib (#3662) 2022-04-21 21:47:10 +09:00
4dd41cffe3 Order swagger's docs tags 2022-04-21 12:30:49 +09:00
9bf3df1e87 Fix Swagger, replacing additionalProperties by allOf 2022-04-21 12:12:30 +09:00
2e08c29c64 Remove replace hardcoded BTC string to default network (#3654) 2022-04-20 10:20:39 +09:00
75afd30008 Design updates (#3653)
* Design updates

Contains:

- btcpayserver/btcpayserver-design#12
- btcpayserver/btcpayserver-design#33
- btcpayserver/btcpayserver-design#35
- btcpayserver/btcpayserver-design#37
- btcpayserver/btcpayserver-design#39

* Improve input focus styles
2022-04-20 09:42:14 +09:00
7652d2c7e3 LN Address - db schema fix method (#3638)
* LN Address - db schema fix method

* pr changes

* shorten
2022-04-19 16:58:31 +09:00
7e6a2d08e2 Add label filter for onchain tx API endpoint (#3588)
* Add label filter for onchain tx API endpoint

close #3587

* Add Swagger docs

* Add test for filtering by transaction label
2022-04-18 11:20:15 +09:00
c1dbe235dc Hide empty plugins section (#3643)
Closes #3617. Implementation of @bolatovumar's [proposal](https://github.com/btcpayserver/btcpayserver/issues/3617#issuecomment-1094173005) - we might be able to replace it with a better solution, that would avoid rendering that section in the first place. For now that solves the issue though.
2022-04-18 10:25:00 +09:00
eb2ccf06a5 Fix typos in README file (#3644)
* Fix typos in README file

* Update BTCPayServer.Tests/README.md

Co-authored-by: d11n <mail@dennisreimann.de>

Co-authored-by: Nicolas Dorier <nicolas.dorier@gmail.com>
Co-authored-by: d11n <mail@dennisreimann.de>
2022-04-18 10:24:17 +09:00
e202f24592 Wallet view: Minor display improvements (#3647)
* Label improvements

* Fix erratic display bug in ToTimeAgo helper
2022-04-18 10:23:32 +09:00
d11f87f0cc Pay Button: Re-add Vue handlers to currency input (#3646)
Fixes a regression introduced in #3642: The pay button preview and code sample do not update without the Vue handlers.
2022-04-16 18:38:51 +02:00
8f54ec4f4a add currency-selector input on Payment Request and Pay Button pages (#3642) 2022-04-15 13:53:37 +09:00
172 changed files with 5565 additions and 1357 deletions

View File

@ -31,9 +31,10 @@
<None Include="icon.png" Pack="true" PackagePath="\" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="HtmlSanitizer" Version="5.0.372" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.1" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.1" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.3" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="6.0.0" />
</ItemGroup>
<ItemGroup>

View File

@ -90,7 +90,7 @@ namespace BTCPayServer.Abstractions.Extensions
public static string ToTimeAgo(this DateTimeOffset date)
{
var diff = DateTimeOffset.UtcNow - date;
var formatted = diff.Seconds > 0
var formatted = diff.TotalSeconds > 0
? $"{diff.TimeString()} ago"
: $"in {diff.Negate().TimeString()}";
return formatted;
@ -113,9 +113,9 @@ namespace BTCPayServer.Abstractions.Extensions
return $"{(int)timeSpan.TotalDays} day{Plural((int)timeSpan.TotalDays)}";
}
private static string Plural(int totalDays)
private static string Plural(int value)
{
return totalDays > 1 ? "s" : string.Empty;
return value > 1 ? "s" : string.Empty;
}
}
}

View File

@ -2,7 +2,7 @@ using Ganss.XSS;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace BTCPayServer.Services
namespace BTCPayServer.Abstractions.Services
{
public class Safe
{

View File

@ -14,7 +14,7 @@
<RepositoryType>git</RepositoryType>
</PropertyGroup>
<PropertyGroup>
<Version Condition=" '$(Version)' == '' ">1.6.0</Version>
<Version Condition=" '$(Version)' == '' ">1.7.0</Version>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<PublishRepositoryUrl>true</PublishRepositoryUrl>

View File

@ -55,7 +55,7 @@ namespace BTCPayServer.Client
}
public virtual async Task<IEnumerable<OnChainWalletTransactionData>> ShowOnChainWalletTransactions(
string storeId, string cryptoCode, TransactionStatus[] statusFilter = null,
string storeId, string cryptoCode, TransactionStatus[] statusFilter = null, string labelFilter = null,
CancellationToken token = default)
{
var query = new Dictionary<string, object>();
@ -63,6 +63,9 @@ namespace BTCPayServer.Client
{
query.Add(nameof(statusFilter), statusFilter);
}
if (labelFilter != null) {
query.Add(nameof(labelFilter), labelFilter);
}
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/transactions", query), token);

View File

@ -0,0 +1,18 @@
#nullable enable
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
namespace BTCPayServer.Client
{
public partial class BTCPayServerClient
{
public virtual async Task<IEnumerable<PayoutProcessorData>> GetPayoutProcessors(
CancellationToken token = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest("api/v1/payout-processors"), token);
return await HandleResponse<IEnumerable<PayoutProcessorData>>(response);
}
}
}

View File

@ -41,12 +41,23 @@ namespace BTCPayServer.Client
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/pull-payments/{HttpUtility.UrlEncode(pullPaymentId)}/payouts", queryPayload: query, method: HttpMethod.Get), cancellationToken);
return await HandleResponse<PayoutData[]>(response);
}
public virtual async Task<PayoutData[]> GetStorePayouts(string storeId, bool includeCancelled = false, CancellationToken cancellationToken = default)
{
Dictionary<string, object> query = new Dictionary<string, object>();
query.Add("includeCancelled", includeCancelled);
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/payouts", queryPayload: query, method: HttpMethod.Get), cancellationToken);
return await HandleResponse<PayoutData[]>(response);
}
public virtual async Task<PayoutData> CreatePayout(string pullPaymentId, CreatePayoutRequest payoutRequest, CancellationToken cancellationToken = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/pull-payments/{HttpUtility.UrlEncode(pullPaymentId)}/payouts", bodyPayload: payoutRequest, method: HttpMethod.Post), cancellationToken);
return await HandleResponse<PayoutData>(response);
}
public virtual async Task<PayoutData> CreatePayout(string storeId, CreatePayoutThroughStoreRequest payoutRequest, CancellationToken cancellationToken = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/payouts", bodyPayload: payoutRequest, method: HttpMethod.Post), cancellationToken);
return await HandleResponse<PayoutData>(response);
}
public virtual async Task CancelPayout(string storeId, string payoutId, CancellationToken cancellationToken = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{HttpUtility.UrlEncode(storeId)}/payouts/{HttpUtility.UrlEncode(payoutId)}", method: HttpMethod.Delete), cancellationToken);

View File

@ -0,0 +1,48 @@
#nullable enable
using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
namespace BTCPayServer.Client
{
public partial class BTCPayServerClient
{
public virtual async Task<IEnumerable<PayoutProcessorData>> GetPayoutProcessors(string storeId,
CancellationToken token = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/payout-processors"), token);
return await HandleResponse<IEnumerable<PayoutProcessorData>>(response);
}
public virtual async Task RemovePayoutProcessor(string storeId, string processor, string paymentMethod, CancellationToken token = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/payout-processors/{processor}/{paymentMethod}", null, HttpMethod.Delete), token);
await HandleResponse(response);
}
public virtual async Task<IEnumerable<LightningAutomatedPayoutSettings>> GetStoreLightningAutomatedPayoutProcessors(string storeId, string? paymentMethod = null,
CancellationToken token = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/payout-processors/LightningAutomatedPayoutSenderFactory{(paymentMethod is null? string.Empty: $"/{paymentMethod}")}"), token);
return await HandleResponse<IEnumerable<LightningAutomatedPayoutSettings>>(response);
}
public virtual async Task<LightningAutomatedPayoutSettings> UpdateStoreLightningAutomatedPayoutProcessors(string storeId, string paymentMethod,LightningAutomatedPayoutSettings request, CancellationToken token = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/payout-processors/LightningAutomatedPayoutSenderFactory/{paymentMethod}",null, request, HttpMethod.Put ), token);
return await HandleResponse<LightningAutomatedPayoutSettings>(response);
}
public virtual async Task<OnChainAutomatedPayoutSettings> UpdateStoreOnChainAutomatedPayoutProcessors(string storeId, string paymentMethod,OnChainAutomatedPayoutSettings request, CancellationToken token = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/payout-processors/OnChainAutomatedPayoutSenderFactory/{paymentMethod}",null, request, HttpMethod.Put ), token);
return await HandleResponse<OnChainAutomatedPayoutSettings>(response);
}
public virtual async Task<IEnumerable<OnChainAutomatedPayoutSettings>> GetStoreOnChainAutomatedPayoutProcessors(string storeId, string? paymentMethod = null,
CancellationToken token = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/payout-processors/OnChainAutomatedPayoutSenderFactory{(paymentMethod is null? string.Empty: $"/{paymentMethod}")}"), token);
return await HandleResponse<IEnumerable<OnChainAutomatedPayoutSettings>>(response);
}
}
}

View File

@ -33,12 +33,10 @@ namespace BTCPayServer.Client
return await HandleResponse<ApplicationUserData>(response);
}
public virtual async Task ToggleUser(string idOrEmail, bool enabled, CancellationToken token = default)
public virtual async Task LockUser(string idOrEmail, bool locked, CancellationToken token = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/users/{idOrEmail}/toggle", null,new
{
enabled
} , HttpMethod.Post), token);
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/users/{idOrEmail}/lock", null,
new LockUserRequest() {Locked = locked}, HttpMethod.Post), token);
await HandleResponse(response);
}

View File

@ -1,7 +0,0 @@
namespace BTCPayServer.Client.Models
{
public class AddCustomerEmailRequest
{
public string Email { get; set; }
}
}

View File

@ -0,0 +1,8 @@
#nullable enable
namespace BTCPayServer.Client.Models;
public class CreatePayoutThroughStoreRequest : CreatePayoutRequest
{
public string? PullPaymentId { get; set; }
public bool? Approved { get; set; }
}

View File

@ -22,5 +22,6 @@ namespace BTCPayServer.Client.Models
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
public DateTimeOffset? StartsAt { get; set; }
public string[] PaymentMethods { get; set; }
public bool AutoApproveClaims { get; set; }
}
}

View File

@ -0,0 +1,13 @@
using System;
using BTCPayServer.Client.JsonConverters;
using Newtonsoft.Json;
namespace BTCPayServer.Client.Models;
public class LightningAutomatedPayoutSettings
{
public string PaymentMethod { get; set; }
[JsonConverter(typeof(TimeSpanJsonConverter.Seconds))]
public TimeSpan IntervalSeconds { get; set; }
}

View File

@ -0,0 +1,6 @@
namespace BTCPayServer.Client;
public class LockUserRequest
{
public bool Locked { get; set; }
}

View File

@ -0,0 +1,13 @@
using System;
using BTCPayServer.Client.JsonConverters;
using Newtonsoft.Json;
namespace BTCPayServer.Client.Models;
public class OnChainAutomatedPayoutSettings
{
public string PaymentMethod { get; set; }
[JsonConverter(typeof(TimeSpanJsonConverter.Seconds))]
public TimeSpan IntervalSeconds { get; set; }
}

View File

@ -1,4 +1,3 @@
#nullable enable
using BTCPayServer.Client.JsonConverters;
using BTCPayServer.JsonConverters;
using NBitcoin;
@ -15,6 +14,6 @@ namespace BTCPayServer.Client.Models
public float? MaxFeePercent { get; set; }
[JsonConverter(typeof(MoneyJsonConverter))]
public Money? MaxFeeFlat { get; set; }
public Money MaxFeeFlat { get; set; }
}
}

View File

@ -0,0 +1,9 @@
namespace BTCPayServer.Client.Models
{
public class PayoutProcessorData
{
public string Name { get; set; }
public string FriendlyName { get; set; }
public string[] PaymentMethods { get; set; }
}
}

View File

@ -31,5 +31,6 @@ namespace BTCPayServer.Client.Models
public TimeSpan BOLT11Expiration { get; set; }
public bool Archived { get; set; }
public string ViewLink { get; set; }
public bool AutoApproveClaims { get; set; }
}
}

View File

@ -1,5 +1,6 @@
using System;
using System.Linq;
using BTCPayServer.Data.Data;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
@ -61,6 +62,8 @@ namespace BTCPayServer.Data
public DbSet<WalletTransactionData> WalletTransactions { get; set; }
public DbSet<WebhookDeliveryData> WebhookDeliveries { get; set; }
public DbSet<WebhookData> Webhooks { get; set; }
public DbSet<LightningAddressData> LightningAddresses{ get; set; }
public DbSet<PayoutProcessorData> PayoutProcessors { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
@ -87,7 +90,7 @@ namespace BTCPayServer.Data
InvoiceData.OnModelCreating(builder);
NotificationData.OnModelCreating(builder);
//OffchainTransactionData.OnModelCreating(builder);
Data.PairedSINData.OnModelCreating(builder);
BTCPayServer.Data.PairedSINData.OnModelCreating(builder);
PairingCodeData.OnModelCreating(builder);
//PayjoinLock.OnModelCreating(builder);
PaymentRequestData.OnModelCreating(builder);
@ -102,10 +105,12 @@ namespace BTCPayServer.Data
//StoreData.OnModelCreating(builder);
U2FDevice.OnModelCreating(builder);
Fido2Credential.OnModelCreating(builder);
Data.UserStore.OnModelCreating(builder);
BTCPayServer.Data.UserStore.OnModelCreating(builder);
//WalletData.OnModelCreating(builder);
WalletTransactionData.OnModelCreating(builder);
WebhookDeliveryData.OnModelCreating(builder);
LightningAddressData.OnModelCreating(builder);
PayoutProcessorData.OnModelCreating(builder);
//WebhookData.OnModelCreating(builder);

View File

@ -0,0 +1,31 @@
using Microsoft.EntityFrameworkCore;
namespace BTCPayServer.Data;
public class LightningAddressData
{
public string Username { get; set; }
public string StoreDataId { get; set; }
public byte[] Blob { get; set; }
public StoreData Store { get; set; }
internal static void OnModelCreating(ModelBuilder builder)
{
builder.Entity<LightningAddressData>()
.HasOne(o => o.Store)
.WithMany(a => a.LightningAddresses)
.HasForeignKey(data => data.StoreDataId)
.IsRequired()
.OnDelete(DeleteBehavior.Cascade);
builder.Entity<LightningAddressData>().HasKey(o => o.Username);
}
}
public class LightningAddressDataBlob
{
public string CurrencyCode { get; set; }
public decimal? Min { get; set; }
public decimal? Max { get; set; }
}

View File

@ -14,6 +14,7 @@ namespace BTCPayServer.Data
public string Id { get; set; }
public DateTimeOffset Date { get; set; }
public string PullPaymentDataId { get; set; }
public string StoreDataId { get; set; }
public PullPaymentData PullPaymentData { get; set; }
[MaxLength(20)]
public PayoutState State { get; set; }
@ -25,12 +26,16 @@ namespace BTCPayServer.Data
#nullable enable
public string? Destination { get; set; }
#nullable restore
public StoreData StoreData { get; set; }
internal static void OnModelCreating(ModelBuilder builder)
{
builder.Entity<PayoutData>()
.HasOne(o => o.PullPaymentData)
.WithMany(o => o.Payouts).OnDelete(DeleteBehavior.Cascade);
builder.Entity<PayoutData>()
.HasOne(o => o.StoreData)
.WithMany(o => o.Payouts).OnDelete(DeleteBehavior.Cascade);
builder.Entity<PayoutData>()
.Property(o => o.State)
.HasConversion<string>();

View File

@ -0,0 +1,24 @@
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
namespace BTCPayServer.Data.Data;
public class PayoutProcessorData
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public string Id { get; set; }
public string StoreId { get; set; }
public StoreData Store { get; set; }
public string PaymentMethod { get; set; }
public string Processor { get; set; }
public byte[] Blob { get; set; }
internal static void OnModelCreating(ModelBuilder builder)
{
builder.Entity<PayoutProcessorData>()
.HasOne(o => o.Store)
.WithMany(data => data.PayoutProcessors).OnDelete(DeleteBehavior.Cascade);
}
}

View File

@ -2,6 +2,8 @@ using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using BTCPayServer.Client.Models;
using BTCPayServer.Data.Data;
using PayoutProcessorData = BTCPayServer.Data.Data.PayoutProcessorData;
namespace BTCPayServer.Data
{
@ -41,5 +43,8 @@ namespace BTCPayServer.Data
public List<PairedSINData> PairedSINs { get; set; }
public IEnumerable<APIKeyData> APIKeys { get; set; }
public IEnumerable<LightningAddressData> LightningAddresses { get; set; }
public IEnumerable<PayoutProcessorData> PayoutProcessors { get; set; }
public IEnumerable<PayoutData> Payouts { get; set; }
}
}

View File

@ -0,0 +1,92 @@
// <auto-generated />
using System;
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace BTCPayServer.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20220311135252_AddPayoutProcessors")]
public partial class AddPayoutProcessors : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "StoreDataId",
table: "Payouts",
type: "TEXT",
nullable: true);
migrationBuilder.CreateTable(
name: "PayoutProcessors",
columns: table => new
{
Id = table.Column<string>(type: "TEXT", nullable: false),
StoreId = table.Column<string>(type: "TEXT", nullable: true),
PaymentMethod = table.Column<string>(type: "TEXT", nullable: true),
Processor = table.Column<string>(type: "TEXT", nullable: true),
Blob = table.Column<byte[]>(nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_PayoutProcessors", x => x.Id);
table.ForeignKey(
name: "FK_PayoutProcessors_Stores_StoreId",
column: x => x.StoreId,
principalTable: "Stores",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_Payouts_StoreDataId",
table: "Payouts",
column: "StoreDataId");
migrationBuilder.CreateIndex(
name: "IX_PayoutProcessors_StoreId",
table: "PayoutProcessors",
column: "StoreId");
if (this.SupportAddForeignKey(ActiveProvider))
{
migrationBuilder.AddForeignKey(
name: "FK_Payouts_Stores_StoreDataId",
table: "Payouts",
column: "StoreDataId",
principalTable: "Stores",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
}
}
protected override void Down(MigrationBuilder migrationBuilder)
{
if (this.SupportDropForeignKey(ActiveProvider))
{
migrationBuilder.DropForeignKey(
name: "FK_Payouts_Stores_StoreDataId",
table: "Payouts");
migrationBuilder.DropTable(
name: "PayoutProcessors");
migrationBuilder.DropIndex(
name: "IX_Payouts_StoreDataId",
table: "Payouts");
}
if(this.SupportDropColumn(ActiveProvider))
{
migrationBuilder.DropColumn(
name: "StoreDataId",
table: "Payouts");
}
}
}
}

View File

@ -0,0 +1,51 @@
// <auto-generated />
using System;
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace BTCPayServer.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20220414132313_AddLightningAddress")]
public partial class AddLightningAddress : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "LightningAddresses",
columns: table => new
{
Username = table.Column<string>(type: "TEXT", nullable: false),
StoreDataId = table.Column<string>(type: "TEXT", nullable: false),
Blob = table.Column<byte[]>( nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_LightningAddresses", x => x.Username);
table.ForeignKey(
name: "FK_LightningAddresses_Stores_StoreDataId",
column: x => x.StoreDataId,
principalTable: "Stores",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_LightningAddresses_StoreDataId",
table: "LightningAddresses",
column: "StoreDataId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "LightningAddresses");
}
}
}

View File

@ -1,4 +1,4 @@
// <auto-generated />
// <auto-generated />
using System;
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore;
@ -6,6 +6,8 @@ using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace BTCPayServer.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
@ -14,40 +16,7 @@ namespace BTCPayServer.Migrations
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "3.1.19");
modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasMaxLength(50);
b.Property<byte[]>("Blob")
.HasColumnType("BLOB");
b.Property<string>("Label")
.HasColumnType("TEXT");
b.Property<string>("StoreId")
.HasColumnType("TEXT")
.HasMaxLength(50);
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.Property<string>("UserId")
.HasColumnType("TEXT")
.HasMaxLength(50);
b.HasKey("Id");
b.HasIndex("StoreId");
b.HasIndex("UserId");
b.ToTable("ApiKeys");
});
modelBuilder.HasAnnotation("ProductVersion", "6.0.1");
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
{
@ -67,6 +36,38 @@ namespace BTCPayServer.Migrations
b.ToTable("AddressInvoices");
});
modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b =>
{
b.Property<string>("Id")
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<byte[]>("Blob")
.HasColumnType("BLOB");
b.Property<string>("Label")
.HasColumnType("TEXT");
b.Property<string>("StoreId")
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.Property<string>("UserId")
.HasMaxLength(50)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("StoreId");
b.HasIndex("UserId");
b.ToTable("ApiKeys");
});
modelBuilder.Entity("BTCPayServer.Data.AppData", b =>
{
b.Property<string>("Id")
@ -119,8 +120,8 @@ namespace BTCPayServer.Migrations
.HasColumnType("TEXT");
b.Property<string>("Email")
.HasColumnType("TEXT")
.HasMaxLength(256);
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<bool>("EmailConfirmed")
.HasColumnType("INTEGER");
@ -132,12 +133,12 @@ namespace BTCPayServer.Migrations
.HasColumnType("TEXT");
b.Property<string>("NormalizedEmail")
.HasColumnType("TEXT")
.HasMaxLength(256);
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("NormalizedUserName")
.HasColumnType("TEXT")
.HasMaxLength(256);
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("PasswordHash")
.HasColumnType("TEXT");
@ -158,19 +159,19 @@ namespace BTCPayServer.Migrations
.HasColumnType("INTEGER");
b.Property<string>("UserName")
.HasColumnType("TEXT")
.HasMaxLength(256);
.HasMaxLength(256)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasName("EmailIndex");
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasName("UserNameIndex");
.HasDatabaseName("UserNameIndex");
b.ToTable("AspNetUsers");
b.ToTable("AspNetUsers", (string)null);
});
modelBuilder.Entity("BTCPayServer.Data.Fido2Credential", b =>
@ -331,16 +332,35 @@ namespace BTCPayServer.Migrations
b.ToTable("InvoiceWebhookDeliveries");
});
modelBuilder.Entity("BTCPayServer.Data.LightningAddressData", b =>
{
b.Property<string>("Username")
.HasColumnType("TEXT");
b.Property<byte[]>("Blob")
.HasColumnType("BLOB");
b.Property<string>("StoreDataId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Username");
b.HasIndex("StoreDataId");
b.ToTable("LightningAddresses");
});
modelBuilder.Entity("BTCPayServer.Data.NotificationData", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasMaxLength(36);
.HasMaxLength(36)
.HasColumnType("TEXT");
b.Property<string>("ApplicationUserId")
.IsRequired()
.HasColumnType("TEXT")
.HasMaxLength(50);
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<byte[]>("Blob")
.HasColumnType("BLOB");
@ -350,8 +370,8 @@ namespace BTCPayServer.Migrations
b.Property<string>("NotificationType")
.IsRequired()
.HasColumnType("TEXT")
.HasMaxLength(100);
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<bool>("Seen")
.HasColumnType("INTEGER");
@ -366,8 +386,8 @@ namespace BTCPayServer.Migrations
modelBuilder.Entity("BTCPayServer.Data.OffchainTransactionData", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasMaxLength(64);
.HasMaxLength(64)
.HasColumnType("TEXT");
b.Property<byte[]>("Blob")
.HasColumnType("BLOB");
@ -437,8 +457,8 @@ namespace BTCPayServer.Migrations
modelBuilder.Entity("BTCPayServer.Data.PayjoinLock", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasMaxLength(100);
.HasMaxLength(100)
.HasColumnType("TEXT");
b.HasKey("Id");
@ -500,8 +520,8 @@ namespace BTCPayServer.Migrations
modelBuilder.Entity("BTCPayServer.Data.PayoutData", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasMaxLength(30);
.HasMaxLength(30)
.HasColumnType("TEXT");
b.Property<byte[]>("Blob")
.HasColumnType("BLOB");
@ -514,8 +534,8 @@ namespace BTCPayServer.Migrations
b.Property<string>("PaymentMethodId")
.IsRequired()
.HasColumnType("TEXT")
.HasMaxLength(20);
.HasMaxLength(20)
.HasColumnType("TEXT");
b.Property<byte[]>("Proof")
.HasColumnType("BLOB");
@ -525,8 +545,11 @@ namespace BTCPayServer.Migrations
b.Property<string>("State")
.IsRequired()
.HasColumnType("TEXT")
.HasMaxLength(20);
.HasMaxLength(20)
.HasColumnType("TEXT");
b.Property<string>("StoreDataId")
.HasColumnType("TEXT");
b.HasKey("Id");
@ -534,6 +557,8 @@ namespace BTCPayServer.Migrations
b.HasIndex("State");
b.HasIndex("StoreDataId");
b.HasIndex("Destination", "State");
b.ToTable("Payouts");
@ -552,8 +577,8 @@ namespace BTCPayServer.Migrations
modelBuilder.Entity("BTCPayServer.Data.PlannedTransaction", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasMaxLength(100);
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<byte[]>("Blob")
.HasColumnType("BLOB");
@ -569,8 +594,8 @@ namespace BTCPayServer.Migrations
modelBuilder.Entity("BTCPayServer.Data.PullPaymentData", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasMaxLength(30);
.HasMaxLength(30)
.HasColumnType("TEXT");
b.Property<bool>("Archived")
.HasColumnType("INTEGER");
@ -588,8 +613,8 @@ namespace BTCPayServer.Migrations
.HasColumnType("TEXT");
b.Property<string>("StoreId")
.HasColumnType("TEXT")
.HasMaxLength(50);
.HasMaxLength(50)
.HasColumnType("TEXT");
b.HasKey("Id");
@ -604,8 +629,8 @@ namespace BTCPayServer.Migrations
.HasColumnType("TEXT");
b.Property<string>("PullPaymentDataId")
.HasColumnType("TEXT")
.HasMaxLength(30);
.HasMaxLength(30)
.HasColumnType("TEXT");
b.HasKey("InvoiceDataId", "PullPaymentDataId");
@ -661,25 +686,6 @@ namespace BTCPayServer.Migrations
b.ToTable("Stores");
});
modelBuilder.Entity("BTCPayServer.Data.StoreWebhookData", b =>
{
b.Property<string>("StoreId")
.HasColumnType("TEXT");
b.Property<string>("WebhookId")
.HasColumnType("TEXT");
b.HasKey("StoreId", "WebhookId");
b.HasIndex("StoreId")
.IsUnique();
b.HasIndex("WebhookId")
.IsUnique();
b.ToTable("StoreWebhooks");
});
modelBuilder.Entity("BTCPayServer.Data.StoredFile", b =>
{
b.Property<string>("Id")
@ -705,6 +711,25 @@ namespace BTCPayServer.Migrations
b.ToTable("Files");
});
modelBuilder.Entity("BTCPayServer.Data.StoreWebhookData", b =>
{
b.Property<string>("StoreId")
.HasColumnType("TEXT");
b.Property<string>("WebhookId")
.HasColumnType("TEXT");
b.HasKey("StoreId", "WebhookId");
b.HasIndex("StoreId")
.IsUnique();
b.HasIndex("WebhookId")
.IsUnique();
b.ToTable("StoreWebhooks");
});
modelBuilder.Entity("BTCPayServer.Data.U2FDevice", b =>
{
b.Property<string>("Id")
@ -791,8 +816,8 @@ namespace BTCPayServer.Migrations
modelBuilder.Entity("BTCPayServer.Data.WebhookData", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasMaxLength(25);
.HasMaxLength(25)
.HasColumnType("TEXT");
b.Property<byte[]>("Blob")
.IsRequired()
@ -806,8 +831,8 @@ namespace BTCPayServer.Migrations
modelBuilder.Entity("BTCPayServer.Data.WebhookDeliveryData", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasMaxLength(25);
.HasMaxLength(25)
.HasColumnType("TEXT");
b.Property<byte[]>("Blob")
.IsRequired()
@ -818,8 +843,8 @@ namespace BTCPayServer.Migrations
b.Property<string>("WebhookId")
.IsRequired()
.HasColumnType("TEXT")
.HasMaxLength(25);
.HasMaxLength(25)
.HasColumnType("TEXT");
b.HasKey("Id");
@ -828,6 +853,31 @@ namespace BTCPayServer.Migrations
b.ToTable("WebhookDeliveries");
});
modelBuilder.Entity("BTCPayServer.PayoutProcessors.PayoutProcessorData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<byte[]>("Blob")
.HasColumnType("BLOB");
b.Property<string>("PaymentMethod")
.HasColumnType("TEXT");
b.Property<string>("Processor")
.HasColumnType("TEXT");
b.Property<string>("StoreId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("StoreId");
b.ToTable("PayoutProcessors");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
@ -838,20 +888,20 @@ namespace BTCPayServer.Migrations
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT")
.HasMaxLength(256);
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("NormalizedName")
.HasColumnType("TEXT")
.HasMaxLength(256);
.HasMaxLength(256)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasName("RoleNameIndex");
.HasDatabaseName("RoleNameIndex");
b.ToTable("AspNetRoles");
b.ToTable("AspNetRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
@ -874,7 +924,7 @@ namespace BTCPayServer.Migrations
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims");
b.ToTable("AspNetRoleClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
@ -897,7 +947,7 @@ namespace BTCPayServer.Migrations
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims");
b.ToTable("AspNetUserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
@ -919,7 +969,7 @@ namespace BTCPayServer.Migrations
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins");
b.ToTable("AspNetUserLogins", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
@ -934,7 +984,7 @@ namespace BTCPayServer.Migrations
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles");
b.ToTable("AspNetUserRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
@ -953,7 +1003,17 @@ namespace BTCPayServer.Migrations
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens");
b.ToTable("AspNetUserTokens", (string)null);
});
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("AddressInvoices")
.HasForeignKey("InvoiceDataId")
.OnDelete(DeleteBehavior.Cascade);
b.Navigation("InvoiceData");
});
modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b =>
@ -967,14 +1027,10 @@ namespace BTCPayServer.Migrations
.WithMany("APIKeys")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("AddressInvoices")
.HasForeignKey("InvoiceDataId")
.OnDelete(DeleteBehavior.Cascade);
b.Navigation("StoreData");
b.Navigation("User");
});
modelBuilder.Entity("BTCPayServer.Data.AppData", b =>
@ -983,6 +1039,8 @@ namespace BTCPayServer.Migrations
.WithMany("Apps")
.HasForeignKey("StoreDataId")
.OnDelete(DeleteBehavior.Cascade);
b.Navigation("StoreData");
});
modelBuilder.Entity("BTCPayServer.Data.Fido2Credential", b =>
@ -991,6 +1049,8 @@ namespace BTCPayServer.Migrations
.WithMany("Fido2Credentials")
.HasForeignKey("ApplicationUserId")
.OnDelete(DeleteBehavior.Cascade);
b.Navigation("ApplicationUser");
});
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
@ -1000,6 +1060,8 @@ namespace BTCPayServer.Migrations
.HasForeignKey("InvoiceDataId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("InvoiceData");
});
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
@ -1012,6 +1074,10 @@ namespace BTCPayServer.Migrations
b.HasOne("BTCPayServer.Data.RefundData", "CurrentRefund")
.WithMany()
.HasForeignKey("Id", "CurrentRefundId");
b.Navigation("CurrentRefund");
b.Navigation("StoreData");
});
modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b =>
@ -1021,6 +1087,8 @@ namespace BTCPayServer.Migrations
.HasForeignKey("InvoiceDataId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("InvoiceData");
});
modelBuilder.Entity("BTCPayServer.Data.InvoiceSearchData", b =>
@ -1029,6 +1097,8 @@ namespace BTCPayServer.Migrations
.WithMany("InvoiceSearchData")
.HasForeignKey("InvoiceDataId")
.OnDelete(DeleteBehavior.Cascade);
b.Navigation("InvoiceData");
});
modelBuilder.Entity("BTCPayServer.Data.InvoiceWebhookDeliveryData", b =>
@ -1044,6 +1114,21 @@ namespace BTCPayServer.Migrations
.HasForeignKey("BTCPayServer.Data.InvoiceWebhookDeliveryData", "InvoiceId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Delivery");
b.Navigation("Invoice");
});
modelBuilder.Entity("BTCPayServer.Data.LightningAddressData", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "Store")
.WithMany("LightningAddresses")
.HasForeignKey("StoreDataId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Store");
});
modelBuilder.Entity("BTCPayServer.Data.NotificationData", b =>
@ -1053,6 +1138,8 @@ namespace BTCPayServer.Migrations
.HasForeignKey("ApplicationUserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ApplicationUser");
});
modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b =>
@ -1061,6 +1148,8 @@ namespace BTCPayServer.Migrations
.WithMany("PairedSINs")
.HasForeignKey("StoreDataId")
.OnDelete(DeleteBehavior.Cascade);
b.Navigation("StoreData");
});
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
@ -1069,6 +1158,8 @@ namespace BTCPayServer.Migrations
.WithMany("Payments")
.HasForeignKey("InvoiceDataId")
.OnDelete(DeleteBehavior.Cascade);
b.Navigation("InvoiceData");
});
modelBuilder.Entity("BTCPayServer.Data.PaymentRequestData", b =>
@ -1077,6 +1168,8 @@ namespace BTCPayServer.Migrations
.WithMany("PaymentRequests")
.HasForeignKey("StoreDataId")
.OnDelete(DeleteBehavior.Cascade);
b.Navigation("StoreData");
});
modelBuilder.Entity("BTCPayServer.Data.PayoutData", b =>
@ -1085,6 +1178,15 @@ namespace BTCPayServer.Migrations
.WithMany("Payouts")
.HasForeignKey("PullPaymentDataId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany("Payouts")
.HasForeignKey("StoreDataId")
.OnDelete(DeleteBehavior.Cascade);
b.Navigation("PullPaymentData");
b.Navigation("StoreData");
});
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
@ -1094,6 +1196,8 @@ namespace BTCPayServer.Migrations
.HasForeignKey("Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("InvoiceData");
});
modelBuilder.Entity("BTCPayServer.Data.PullPaymentData", b =>
@ -1102,6 +1206,8 @@ namespace BTCPayServer.Migrations
.WithMany("PullPayments")
.HasForeignKey("StoreId")
.OnDelete(DeleteBehavior.Cascade);
b.Navigation("StoreData");
});
modelBuilder.Entity("BTCPayServer.Data.RefundData", b =>
@ -1117,6 +1223,19 @@ namespace BTCPayServer.Migrations
.HasForeignKey("PullPaymentDataId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("InvoiceData");
b.Navigation("PullPaymentData");
});
modelBuilder.Entity("BTCPayServer.Data.StoredFile", b =>
{
b.HasOne("BTCPayServer.Data.ApplicationUser", "ApplicationUser")
.WithMany("StoredFiles")
.HasForeignKey("ApplicationUserId");
b.Navigation("ApplicationUser");
});
modelBuilder.Entity("BTCPayServer.Data.StoreWebhookData", b =>
@ -1132,13 +1251,10 @@ namespace BTCPayServer.Migrations
.HasForeignKey("BTCPayServer.Data.StoreWebhookData", "WebhookId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("BTCPayServer.Data.StoredFile", b =>
{
b.HasOne("BTCPayServer.Data.ApplicationUser", "ApplicationUser")
.WithMany("StoredFiles")
.HasForeignKey("ApplicationUserId");
b.Navigation("Store");
b.Navigation("Webhook");
});
modelBuilder.Entity("BTCPayServer.Data.U2FDevice", b =>
@ -1147,6 +1263,8 @@ namespace BTCPayServer.Migrations
.WithMany("U2FDevices")
.HasForeignKey("ApplicationUserId")
.OnDelete(DeleteBehavior.Cascade);
b.Navigation("ApplicationUser");
});
modelBuilder.Entity("BTCPayServer.Data.UserStore", b =>
@ -1162,6 +1280,10 @@ namespace BTCPayServer.Migrations
.HasForeignKey("StoreDataId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ApplicationUser");
b.Navigation("StoreData");
});
modelBuilder.Entity("BTCPayServer.Data.WalletTransactionData", b =>
@ -1171,6 +1293,8 @@ namespace BTCPayServer.Migrations
.HasForeignKey("WalletDataId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("WalletData");
});
modelBuilder.Entity("BTCPayServer.Data.WebhookDeliveryData", b =>
@ -1180,6 +1304,18 @@ namespace BTCPayServer.Migrations
.HasForeignKey("WebhookId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Webhook");
});
modelBuilder.Entity("BTCPayServer.PayoutProcessors.PayoutProcessorData", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "Store")
.WithMany("PayoutProcessors")
.HasForeignKey("StoreId")
.OnDelete(DeleteBehavior.Cascade);
b.Navigation("Store");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
@ -1218,7 +1354,7 @@ namespace BTCPayServer.Migrations
.IsRequired();
b.HasOne("BTCPayServer.Data.ApplicationUser", null)
.WithMany()
.WithMany("UserRoles")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
@ -1232,6 +1368,78 @@ namespace BTCPayServer.Migrations
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("BTCPayServer.Data.ApplicationUser", b =>
{
b.Navigation("APIKeys");
b.Navigation("Fido2Credentials");
b.Navigation("Notifications");
b.Navigation("StoredFiles");
b.Navigation("U2FDevices");
b.Navigation("UserRoles");
b.Navigation("UserStores");
});
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
{
b.Navigation("AddressInvoices");
b.Navigation("Events");
b.Navigation("HistoricalAddressInvoices");
b.Navigation("InvoiceSearchData");
b.Navigation("Payments");
b.Navigation("PendingInvoices");
b.Navigation("Refunds");
});
modelBuilder.Entity("BTCPayServer.Data.PullPaymentData", b =>
{
b.Navigation("Payouts");
});
modelBuilder.Entity("BTCPayServer.Data.StoreData", b =>
{
b.Navigation("APIKeys");
b.Navigation("Apps");
b.Navigation("Invoices");
b.Navigation("LightningAddresses");
b.Navigation("PairedSINs");
b.Navigation("PaymentRequests");
b.Navigation("Payouts");
b.Navigation("PullPayments");
b.Navigation("PayoutProcessors");
b.Navigation("UserStores");
});
modelBuilder.Entity("BTCPayServer.Data.WalletData", b =>
{
b.Navigation("WalletTransactions");
});
modelBuilder.Entity("BTCPayServer.Data.WebhookData", b =>
{
b.Navigation("Deliveries");
});
#pragma warning restore 612, 618
}
}

View File

@ -1,16 +1,22 @@
using System;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Contracts;
using NBitcoin.Crypto;
using NBitcoin.DataEncoders;
using NBitcoin.Secp256k1;
namespace BTCPayServer.PluginPacker
{
class Program
{
static void Main(string[] args)
static async Task Main(string[] args)
{
if (args.Length < 3)
{
@ -19,7 +25,7 @@ namespace BTCPayServer.PluginPacker
}
var directory = args[0];
var name = args[1];
var outputDir = args[2];
var outputDir = Path.Combine(args[2], name);
var outputFile = Path.Combine(outputDir, name);
var rootDLLPath = Path.Combine(directory, name + ".dll");
if (!File.Exists(rootDLLPath))
@ -37,12 +43,51 @@ namespace BTCPayServer.PluginPacker
var loadedPlugin = (IBTCPayServerPlugin)Activator.CreateInstance(extension);
var json = JsonSerializer.Serialize(loadedPlugin);
Directory.CreateDirectory(outputDir);
outputDir = Path.Combine(outputDir, loadedPlugin.Version.ToString());
Directory.CreateDirectory(outputDir);
outputFile = Path.Combine(outputDir, name);
if (File.Exists(outputFile + ".btcpay"))
{
File.Delete(outputFile + ".btcpay");
}
ZipFile.CreateFromDirectory(directory, outputFile + ".btcpay", CompressionLevel.Optimal, false);
File.WriteAllText(outputFile + ".btcpay.json", json);
await File.WriteAllTextAsync(outputFile + ".btcpay.json", json);
var sha256sums = new StringBuilder();
sha256sums.AppendLine(
$"{Encoders.Hex.EncodeData(Hashes.SHA256(Encoding.UTF8.GetBytes(json)))} {name}.btcpay.json");
sha256sums.AppendLine(
$"{Encoders.Hex.EncodeData(Hashes.SHA256(await File.ReadAllBytesAsync(outputFile + ".btcpay")))} {name}.btcpay");
var sha256dirs = Path.Combine(outputDir, "SHA256SUMS");
if (File.Exists(sha256dirs))
{
File.Delete(sha256dirs);
}
await File.WriteAllTextAsync(sha256dirs, sha256sums.ToString());
try
{
Process cmd = new();
cmd.StartInfo.FileName = "powershell.exe";
cmd.StartInfo.RedirectStandardInput = true;
cmd.StartInfo.RedirectStandardOutput = true;
cmd.StartInfo.CreateNoWindow = false;
cmd.StartInfo.UseShellExecute = false;
cmd.Start();
await cmd.StandardInput.WriteLineAsync($"cat {sha256dirs} | gpg -s > {Path.Combine(outputDir, "SHA256SUMS.asc")}");
await cmd.StandardInput.FlushAsync();
cmd.StandardInput.Close();
await cmd.WaitForExitAsync();
Console.WriteLine(await cmd.StandardOutput.ReadToEndAsync());
}
catch (Exception e)
{
Console.WriteLine($"Attempted to sign hashes with gpg but maybe powershell is not installed?\n{e.Message}");
}
Console.WriteLine($"Created {outputFile}.btcpay at {directory}");
}

View File

@ -21,9 +21,9 @@
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.14" />
<PackageReference Include="Selenium.Support" Version="3.141.0" />
<PackageReference Include="Selenium.WebDriver" Version="3.141.0" />
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="98.0.4758.10200" />
<PackageReference Include="Selenium.Support" Version="4.1.1" />
<PackageReference Include="Selenium.WebDriver" Version="4.1.1" />
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="101.0.4951.4100" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<PrivateAssets>all</PrivateAssets>

View File

@ -449,10 +449,10 @@ namespace BTCPayServer.Tests
await tester.StartAsync();
var acc = tester.NewAccount();
acc.Register();
acc.CreateStore();
await acc.CreateStoreAsync();
var storeId = (await acc.RegisterDerivationSchemeAsync("BTC", importKeysToNBX: true)).StoreId;
var client = await acc.CreateClient();
var result = await client.CreatePullPayment(storeId, new Client.Models.CreatePullPaymentRequest()
var result = await client.CreatePullPayment(storeId, new CreatePullPaymentRequest()
{
Name = "Test",
Description = "Test description",
@ -2163,6 +2163,9 @@ namespace BTCPayServer.Tests
new[] { TransactionStatus.Unconfirmed }), data => data.TransactionHash == txdata.TransactionHash);
Assert.Contains(
await client.ShowOnChainWalletTransactions(walletId.StoreId, walletId.CryptoCode), data => data.TransactionHash == txdata.TransactionHash);
Assert.Contains(
await client.ShowOnChainWalletTransactions(walletId.StoreId, walletId.CryptoCode, null, "test label"), data => data.TransactionHash == txdata.TransactionHash);
await tester.WaitForEvent<NewBlockEvent>(async () =>
{
@ -2337,5 +2340,165 @@ namespace BTCPayServer.Tests
await adminClient.SendEmail(admin.StoreId,
new SendEmailRequest() { Body = "lol", Subject = "subj", Email = "sdasdas" });
}
[Fact(Timeout = 60 * 2 * 1000)]
[Trait("Integration", "Integration")]
public async Task DisabledEnabledUserTests()
{
using var tester = CreateServerTester();
await tester.StartAsync();
var admin = tester.NewAccount();
await admin.GrantAccessAsync(true);
var adminClient = await admin.CreateClient(Policies.Unrestricted);
var newUser = tester.NewAccount();
await newUser.GrantAccessAsync();
var newUserClient = await newUser.CreateClient(Policies.Unrestricted);
Assert.False((await newUserClient.GetCurrentUser()).Disabled);
await adminClient.LockUser(newUser.UserId, true, CancellationToken.None);
Assert.True((await adminClient.GetUserByIdOrEmail(newUser.UserId)).Disabled);
await AssertAPIError("unauthenticated",async () =>
{
await newUserClient.GetCurrentUser();
});
var newUserBasicClient = new BTCPayServerClient(newUserClient.Host, newUser.RegisterDetails.Email,
newUser.RegisterDetails.Password);
await AssertAPIError("unauthenticated",async () =>
{
await newUserBasicClient.GetCurrentUser();
});
await adminClient.LockUser(newUser.UserId, false, CancellationToken.None);
Assert.False((await adminClient.GetUserByIdOrEmail(newUser.UserId)).Disabled);
await newUserClient.GetCurrentUser();
await newUserBasicClient.GetCurrentUser();
// Twice for good measure
await adminClient.LockUser(newUser.UserId, false, CancellationToken.None);
Assert.False((await adminClient.GetUserByIdOrEmail(newUser.UserId)).Disabled);
await newUserClient.GetCurrentUser();
await newUserBasicClient.GetCurrentUser();
}
[Fact(Timeout = 60 * 2 * 1000)]
[Trait("Integration", "Integration")]
public async Task CanUsePayoutProcessorsThroughAPI()
{
using var tester = CreateServerTester();
await tester.StartAsync();
var admin = tester.NewAccount();
await admin.GrantAccessAsync(true);
var adminClient = await admin.CreateClient(Policies.Unrestricted);
var registeredProcessors = await adminClient.GetPayoutProcessors();
Assert.Equal(2,registeredProcessors.Count());
await adminClient.GenerateOnChainWallet(admin.StoreId, "BTC", new GenerateOnChainWalletRequest()
{
SavePrivateKeys = true
});
var preApprovedPayoutWithoutPullPayment = await adminClient.CreatePayout(admin.StoreId, new CreatePayoutThroughStoreRequest()
{
Amount = 0.0001m,
Approved = true,
PaymentMethod = "BTC",
Destination = (await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
});
var notApprovedPayoutWithoutPullPayment = await adminClient.CreatePayout(admin.StoreId, new CreatePayoutThroughStoreRequest()
{
Amount = 0.00001m,
Approved = false,
PaymentMethod = "BTC",
Destination = (await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
});
var pullPayment = await adminClient.CreatePullPayment(admin.StoreId, new CreatePullPaymentRequest()
{
Amount = 100,
Currency = "USD",
Name = "pull payment",
PaymentMethods = new []{ "BTC"}
});
var notapprovedPayoutWithPullPayment = await adminClient.CreatePayout(admin.StoreId, new CreatePayoutThroughStoreRequest()
{
PullPaymentId = pullPayment.Id,
Amount = 10,
Approved = false,
PaymentMethod = "BTC",
Destination = (await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
});
await adminClient.ApprovePayout(admin.StoreId, notapprovedPayoutWithPullPayment.Id,
new ApprovePayoutRequest() { });
var payouts = await adminClient.GetStorePayouts(admin.StoreId);
Assert.Equal(3, payouts.Length);
Assert.Single(payouts, data => data.State == PayoutState.AwaitingApproval);
await adminClient.ApprovePayout(admin.StoreId, notApprovedPayoutWithoutPullPayment.Id,
new ApprovePayoutRequest() { });
payouts = await adminClient.GetStorePayouts(admin.StoreId);
Assert.Equal(3, payouts.Length);
Assert.Empty(payouts.Where(data => data.State == PayoutState.AwaitingApproval));
Assert.Empty(payouts.Where(data => data.PaymentMethodAmount is null));
Assert.Empty( await adminClient.ShowOnChainWalletTransactions(admin.StoreId, "BTC"));
Assert.Empty( await adminClient.GetStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC"));
Assert.Empty(await adminClient.GetPayoutProcessors(admin.StoreId));
await adminClient.UpdateStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC",
new OnChainAutomatedPayoutSettings() {IntervalSeconds = TimeSpan.FromSeconds(100000)});
Assert.Equal(100000, Assert.Single( await adminClient.GetStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC")).IntervalSeconds.TotalSeconds);
var tpGen = Assert.Single(await adminClient.GetPayoutProcessors(admin.StoreId));
Assert.Equal("BTC", Assert.Single(tpGen.PaymentMethods));
//still too poor to process any payouts
Assert.Empty( await adminClient.ShowOnChainWalletTransactions(admin.StoreId, "BTC"));
await adminClient.RemovePayoutProcessor(admin.StoreId, tpGen.Name, tpGen.PaymentMethods.First());
Assert.Empty( await adminClient.GetStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC"));
Assert.Empty(await adminClient.GetPayoutProcessors(admin.StoreId));
await tester.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create((await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
tester.ExplorerClient.Network.NBitcoinNetwork), Money.Coins(0.000012m));
await tester.ExplorerNode.GenerateAsync(1);
await TestUtils.EventuallyAsync(async () =>
{
Assert.Single(await adminClient.ShowOnChainWalletTransactions(admin.StoreId, "BTC"));
payouts = await adminClient.GetStorePayouts(admin.StoreId);
Assert.Equal(3, payouts.Length);
});
await adminClient.UpdateStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC",
new OnChainAutomatedPayoutSettings() {IntervalSeconds = TimeSpan.FromSeconds(5)});
Assert.Equal(5, Assert.Single( await adminClient.GetStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC")).IntervalSeconds.TotalSeconds);
await TestUtils.EventuallyAsync(async () =>
{
Assert.Equal(2, (await adminClient.ShowOnChainWalletTransactions(admin.StoreId, "BTC")).Count());
payouts = await adminClient.GetStorePayouts(admin.StoreId);
Assert.Single(payouts.Where(data => data.State == PayoutState.InProgress));
});
await tester.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create((await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
tester.ExplorerClient.Network.NBitcoinNetwork), Money.Coins(0.01m));
await TestUtils.EventuallyAsync(async () =>
{
Assert.Equal(4, (await adminClient.ShowOnChainWalletTransactions(admin.StoreId, "BTC")).Count());
payouts = await adminClient.GetStorePayouts(admin.StoreId);
Assert.Empty(payouts.Where(data => data.State != PayoutState.InProgress));
});
}
}
}

View File

@ -30,7 +30,7 @@ You can also generate blocks:
- Install and run [Polar](https://lightningpolar.com/). Setup a small network of nodes.
- Go to your store's General Settings and enable Lightning.
- Build your connection string using the Connect infomation in the Polar app.
- Build your connection string using the Connect information in the Polar app.
LND Connection string example:
type=lnd-rest;server=https://127.0.0.1:8084/;macaroonfilepath="local path to admin.macaroon on your computer, without these quotes";allowinsecure=true
@ -38,7 +38,7 @@ type=lnd-rest;server=https://127.0.0.1:8084/;macaroonfilepath="local path to adm
Now you can create a Lightning invoice on BTCPay Server regtest and make a payment through Polar.
PLEASE NOTE: You may get an exception break in Visual Studio. You must quickly click "Continue" in VS so the invoice is generated.
Or, uncheck the box that says, "Break when this exceptiontype is thrown".
Or, uncheck the box that says, "Break when this exception type is thrown".
### Using the test litecoin-cli
@ -88,8 +88,8 @@ Or, uncheck the box that says, "Break when this exception type is thrown".
### `docker-compose up dev` failed or tests are not passing, what should I do?
1. Run `docker-compose down --v` (this will reset your test environment)
2. Run `docker-compose pull` (this will ensure you have the lastest images)
1. Run `docker-compose down --volumes` (this will reset your test environment)
2. Run `docker-compose pull` (this will ensure you have the latest images)
3. Run again with `docker-compose up dev`
### How to run the Altcoin environment?

View File

@ -57,6 +57,8 @@ namespace BTCPayServer.Tests
options.AddArgument("start-maximized");
if (Server.PayTester.InContainer)
{
// Shot in the dark to fix https://stackoverflow.com/questions/53902507/unknown-error-session-deleted-because-of-page-crash-from-unknown-error-cannot
options.AddArgument("--disable-dev-shm-usage");
Driver = new OpenQA.Selenium.Remote.RemoteWebDriver(new Uri("http://selenium:4444/wd/hub"), new RemoteSessionSettings(options));
var containerIp = File.ReadAllText("/etc/hosts").Split('\n', StringSplitOptions.RemoveEmptyEntries).Last()
.Split('\t', StringSplitOptions.RemoveEmptyEntries)[0].Trim();

View File

@ -673,11 +673,12 @@ namespace BTCPayServer.Tests
s.LogIn(userId);
// Make sure after login, we are not redirected to the PoS
Assert.DoesNotContain("Tea shop", s.Driver.PageSource);
var prevUrl = s.Driver.Url;
// We are only if explicitly going to /
s.GoToUrl("/");
Assert.Contains("Tea shop", s.Driver.PageSource);
s.Driver.Navigate().Back();
s.Driver.Navigate().GoToUrl(new Uri(prevUrl, UriKind.Absolute));
// Let's check with domain mapping as well.
s.GoToServer(ServerNavPages.Policies);
@ -1411,6 +1412,28 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id($"{PayoutState.Completed}-view")).Click();
Assert.Contains(bolt, s.Driver.PageSource);
}
//auto-approve pull payments
s.GoToStore(StoreNavPages.PullPayments);
s.Driver.FindElement(By.Id("NewPullPayment")).Click();
s.Driver.FindElement(By.Id("Name")).SendKeys("PP1");
s.Driver.SetCheckbox(By.Id("AutoApproveClaims"), true);
s.Driver.FindElement(By.Id("Amount")).Clear();
s.Driver.FindElement(By.Id("Amount")).SendKeys("99.0" + Keys.Enter);
s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success);
s.Driver.FindElement(By.LinkText("View")).Click();
address = await s.Server.ExplorerNode.GetNewAddressAsync();
s.Driver.FindElement(By.Id("Destination")).Clear();
s.Driver.FindElement(By.Id("Destination")).SendKeys(address.ToString());
s.Driver.FindElement(By.Id("ClaimedAmount")).Clear();
s.Driver.FindElement(By.Id("ClaimedAmount")).SendKeys("20" + Keys.Enter);
s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success);
Assert.Contains(PayoutState.AwaitingPayment.GetStateString(), s.Driver.PageSource);
}
[Fact]
@ -1713,11 +1736,6 @@ namespace BTCPayServer.Tests
s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success);
s.Driver.ToggleCollapse("AddAddress");
s.Driver.FindElement(By.Id("Add_Username")).SendKeys(lnaddress1);
s.Driver.FindElement(By.CssSelector("button[value='add']")).Click();
s.Driver.FindElement(By.ClassName("text-danger"));
s.Driver.FindElement(By.Id("Add_Username")).Clear();
var lnaddress2 = "EUR" + Guid.NewGuid().ToString();
s.Driver.FindElement(By.Id("Add_Username")).SendKeys(lnaddress2);

View File

@ -836,37 +836,6 @@ namespace BTCPayServer.Tests
$"enddate:{time.AddSeconds(-1).ToString("yyyy-MM-dd HH:mm:ss")}");
}
[Fact(Timeout = LongRunningTestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanListNotifications()
{
using var tester = CreateServerTester();
await tester.StartAsync();
var acc = tester.NewAccount();
acc.GrantAccess(true);
acc.RegisterDerivationScheme("BTC");
const string newVersion = "1.0.4.4";
var ctrl = acc.GetController<UINotificationsController>();
var resp = await ctrl.Generate(newVersion);
var vm = Assert.IsType<Models.NotificationViewModels.IndexViewModel>(
Assert.IsType<ViewResult>(await ctrl.Index()).Model);
Assert.True(vm.Skip == 0);
Assert.True(vm.Count == 50);
Assert.True(vm.Total == 1);
Assert.True(vm.Items.Count == 1);
var fn = vm.Items.First();
var now = DateTimeOffset.UtcNow;
Assert.True(fn.Created >= now.AddSeconds(-3));
Assert.True(fn.Created <= now);
Assert.Equal($"New version {newVersion} released!", fn.Body);
Assert.Equal($"https://github.com/btcpayserver/btcpayserver/releases/tag/v{newVersion}", fn.ActionLink);
Assert.False(fn.Seen);
}
[Fact]
[Trait("Integration", "Integration")]
public async Task CanGetRates()
@ -2450,7 +2419,7 @@ namespace BTCPayServer.Tests
Assert.True(vm.Skip == 0);
Assert.True(vm.Count == 50);
Assert.True(vm.Total == 1);
Assert.Null(vm.Total);
Assert.True(vm.Items.Count == 1);
var fn = vm.Items.First();

View File

@ -32,7 +32,7 @@ services:
TESTS_SOCKSENDPOINT: "tor:9050"
expose:
- "80"
links:
depends_on:
- dev
- selenium
extra_hosts:
@ -46,7 +46,7 @@ services:
dev:
image: alpine:3.7
command: [ "/bin/sh", "-c", "trap : TERM INT; while :; do echo Ready to code and debug like a rockstar!!!; sleep 2073600; done & wait" ]
links:
depends_on:
- nbxplorer
- postgres
- customer_lightningd
@ -79,13 +79,13 @@ services:
connect=bitcoind:39388
fallbackfee=0.0002
rpcallowip=0.0.0.0/0
links:
depends_on:
- nbxplorer
- postgres
- customer_lnd
- merchant_lnd
selenium:
image: selenium/standalone-chrome:3
image: selenium/standalone-chrome:101.0
expose:
- "4444"
nbxplorer:
@ -117,7 +117,7 @@ services:
NBXPLORER_POSTGRES: User ID=postgres;Include Error Detail=true;Host=postgres;Port=5432;Database=nbxplorer
NBXPLORER_NOAUTH: 1
NBXPLORER_EXPOSERPC: 1
links:
depends_on:
- bitcoind
- litecoind
- elementsd-liquid
@ -176,7 +176,7 @@ services:
volumes:
- "bitcoin_datadir:/etc/bitcoin"
- "customer_lightningd_datadir:/root/.lightning"
links:
depends_on:
- bitcoind
lightning-charged:
@ -197,7 +197,7 @@ services:
- "9735" # Lightning
ports:
- "54938:9112" # Charge
links:
depends_on:
- bitcoind
- merchant_lightningd
@ -224,7 +224,7 @@ services:
volumes:
- "bitcoin_datadir:/etc/bitcoin"
- "merchant_lightningd_datadir:/root/.lightning"
links:
depends_on:
- bitcoind
postgres:
image: postgres:13.4
@ -266,7 +266,7 @@ services:
volumes:
- "merchant_lnd_datadir:/data"
- "bitcoin_datadir:/deps/.bitcoin"
links:
depends_on:
- bitcoind
customer_lnd:
@ -301,7 +301,7 @@ services:
volumes:
- "customer_lnd_datadir:/root/.lnd"
- "bitcoin_datadir:/deps/.bitcoin"
links:
depends_on:
- bitcoind
tor:

View File

@ -30,7 +30,7 @@ services:
TESTS_SOCKSENDPOINT: "tor:9050"
expose:
- "80"
links:
depends_on:
- dev
- selenium
extra_hosts:
@ -44,7 +44,7 @@ services:
dev:
image: alpine:3.7
command: [ "/bin/sh", "-c", "trap : TERM INT; while :; do echo Ready to code and debug like a rockstar!!!; sleep 2073600; done & wait" ]
links:
depends_on:
- nbxplorer
- postgres
- customer_lightningd
@ -76,13 +76,13 @@ services:
connect=bitcoind:39388
rpcallowip=0.0.0.0/0
fallbackfee=0.0002
links:
depends_on:
- nbxplorer
- postgres
- customer_lnd
- merchant_lnd
selenium:
image: selenium/standalone-chrome:3
image: selenium/standalone-chrome:101.0
expose:
- "4444"
nbxplorer:
@ -106,7 +106,7 @@ services:
NBXPLORER_POSTGRES: User ID=postgres;Include Error Detail=true;Host=postgres;Port=5432;Database=nbxplorer
NBXPLORER_EXPOSERPC: 1
NBXPLORER_NOAUTH: 1
links:
depends_on:
- bitcoind
@ -163,7 +163,7 @@ services:
volumes:
- "bitcoin_datadir:/etc/bitcoin"
- "customer_lightningd_datadir:/root/.lightning"
links:
depends_on:
- bitcoind
lightning-charged:
@ -184,7 +184,7 @@ services:
- "9735" # Lightning
ports:
- "54938:9112" # Charge
links:
depends_on:
- bitcoind
- merchant_lightningd
@ -211,7 +211,7 @@ services:
volumes:
- "bitcoin_datadir:/etc/bitcoin"
- "merchant_lightningd_datadir:/root/.lightning"
links:
depends_on:
- bitcoind
postgres:
@ -256,7 +256,7 @@ services:
volumes:
- "merchant_lnd_datadir:/data"
- "bitcoin_datadir:/deps/.bitcoin"
links:
depends_on:
- bitcoind
customer_lnd:
@ -292,7 +292,7 @@ services:
volumes:
- "customer_lnd_datadir:/root/.lnd"
- "bitcoin_datadir:/deps/.bitcoin"
links:
depends_on:
- bitcoind
tor:

View File

@ -48,7 +48,7 @@
<ItemGroup>
<PackageReference Include="BIP78.Sender" Version="0.2.2" />
<PackageReference Include="BTCPayServer.Hwi" Version="2.0.2" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.3.4" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.3.6" />
<PackageReference Include="BuildBundlerMinifier" Version="3.2.449" />
<PackageReference Include="BundlerMinifier.Core" Version="3.2.435" />
<PackageReference Include="BundlerMinifier.TagHelpers" Version="3.2.435" />
@ -57,7 +57,7 @@
<PackageReference Include="Fido2" Version="2.0.1" />
<PackageReference Include="Fido2.AspNet" Version="2.0.1" />
<PackageReference Include="HtmlSanitizer" Version="5.0.372" />
<PackageReference Include="LNURL" Version="0.0.15" />
<PackageReference Include="LNURL" Version="0.0.18" />
<PackageReference Include="MailKit" Version="3.0.0" />
<PackageReference Include="McMaster.NETCore.Plugins.Mvc" Version="1.4.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />

View File

@ -197,17 +197,26 @@
</div>
</div>
<script>
const navCollapsed = window.localStorage.getItem('btcpay-nav-collapsed')
const collapsed = navCollapsed ? JSON.parse(navCollapsed) : []
collapsed.forEach(id => {
const el = document.getElementById(id)
const btn = el && el.previousElementSibling.querySelector(`[aria-controls="${id}"]`)
if (el && btn) {
el.classList.remove('show')
btn.classList.add('collapsed')
btn.setAttribute('aria-expanded', 'false')
(() => {
// apply collapse settings
const navCollapsed = window.localStorage.getItem('btcpay-nav-collapsed')
const collapsed = navCollapsed ? JSON.parse(navCollapsed) : []
collapsed.forEach(id => {
const el = document.getElementById(id)
const btn = el && el.previousElementSibling.querySelector(`[aria-controls="${id}"]`)
if (el && btn) {
el.classList.remove('show')
btn.classList.add('collapsed')
btn.setAttribute('aria-expanded', 'false')
}
})
// hide empty plugins drawer
const pluginsItem = document.getElementById('Nav-Plugins').closest('.accordion-item')
const pluginsContent = pluginsItem.querySelector('.navbar-nav').innerHTML.trim()
if (pluginsContent === '') {
pluginsItem.setAttribute('hidden', true)
}
})
})()
</script>
}
else if (Env.IsSecure)

View File

@ -1,39 +1,45 @@
@model BasePagingViewModel
@{
var pageSizeOptions = new [] { 50, 100, 250, 500 };
var pageSizeOptions = new[] { 50, 100, 250, 500 };
}
@if (Model.Total > pageSizeOptions.Min())
@if (Model.Total is null || Model.Total.Value > pageSizeOptions.Min())
{
<nav aria-label="..." class="w-100 clearfix">
@if (Model.Total > Model.Count)
@if (Model.Total is null || Model.Total.Value > Model.Count)
{
<ul class="pagination float-start">
<li class="page-item @(Model.Skip == 0 ? "disabled" : null)">
<a class="page-link" tabindex="-1" href="@NavigatePages(-1, Model.Count)">&laquo;</a>
</li>
<li class="page-item disabled">
@if (Model.Total <= Model.Count)
@if (Model.Total is null)
{
<span class="page-link">
@(Model.Skip + 1)@(Model.Skip + Model.Count)
</span>
}
else if (Model.Total.Value <= Model.Count)
{
<span class="page-link">
1@Model.Total
1@Model.Total.Value
</span>
}
else
{
<span class="page-link">
@(Model.Skip + 1)@(Model.Skip + Model.Count), Total: @Model.Total
@(Model.Skip + 1)@(Model.Skip + Model.Count), Total: @Model.Total.Value
</span>
}
</li>
<li class="page-item @(Model.Total > (Model.Skip + Model.Count) ? null : "disabled")">
<li class="page-item @(((Model.Total is null && Model.CurrentPageCount >= Model.Count) || (Model.Total is not null && Model.Total.Value > (Model.Skip + Model.Count))) ? null : "disabled")">
<a class="page-link" href="@NavigatePages(1, Model.Count)">&raquo;</a>
</li>
</ul>
}
@if (Model.Total >= pageSizeOptions.Min())
@if (Model.Total is null || Model.Total.Value >= pageSizeOptions.Min())
{
<ul class="pagination float-end">
<li class="page-item disabled">
@ -41,7 +47,7 @@
</li>
@foreach (int pageSize in pageSizeOptions)
{
if (Model.Total >= pageSize)
if (Model.Total is null || Model.Total.Value >= pageSize)
{
<li class="page-item @(Model.Count == pageSize ? "active" : null)">
<a class="page-link" href="@NavigatePages(0, pageSize)">@pageSize</a>

View File

@ -8,16 +8,19 @@
</header>
<div class="h3">@Model.PayoutsPending</div>
</div>
<div class="store-number">
<header>
<h6>TXs in the last @Model.TransactionDays days</h6>
@if (Model.Transactions > 0)
{
<a asp-controller="UIWallets" asp-action="WalletTransactions" asp-route-walletId="@Model.WalletId">View All</a>
}
</header>
<div class="h3">@Model.Transactions</div>
</div>
@if (Model.Transactions is not null)
{
<div class="store-number">
<header>
<h6>TXs in the last @Model.TransactionDays days</h6>
@if (Model.Transactions.Value > 0)
{
<a asp-controller="UIWallets" asp-action="WalletTransactions" asp-route-walletId="@Model.WalletId">View All</a>
}
</header>
<div class="h3">@Model.Transactions.Value</div>
</div>
}
<div class="store-number">
<header>
<h6>Refunds Issued</h6>

View File

@ -1,10 +1,12 @@
using System;
using Dapper;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
using BTCPayServer.Components.StoreRecentTransactions;
using BTCPayServer.Data;
using BTCPayServer.Services;
using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Wallets;
using Microsoft.AspNetCore.Identity;
@ -17,24 +19,28 @@ namespace BTCPayServer.Components.StoreNumbers;
public class StoreNumbers : ViewComponent
{
private const string CryptoCode = "BTC";
private string CryptoCode;
private const int TransactionDays = 7;
private readonly StoreRepository _storeRepo;
private readonly ApplicationDbContextFactory _dbContextFactory;
private readonly BTCPayWalletProvider _walletProvider;
private readonly NBXplorerConnectionFactory _nbxConnectionFactory;
private readonly BTCPayNetworkProvider _networkProvider;
public StoreNumbers(
StoreRepository storeRepo,
ApplicationDbContextFactory dbContextFactory,
BTCPayNetworkProvider networkProvider,
BTCPayWalletProvider walletProvider)
BTCPayWalletProvider walletProvider,
NBXplorerConnectionFactory nbxConnectionFactory)
{
_storeRepo = storeRepo;
_walletProvider = walletProvider;
_nbxConnectionFactory = nbxConnectionFactory;
_networkProvider = networkProvider;
_dbContextFactory = dbContextFactory;
CryptoCode = networkProvider.DefaultNetwork.CryptoCode;
}
public async Task<IViewComponentResult> InvokeAsync(StoreData store)
@ -50,16 +56,14 @@ public class StoreNumbers : ViewComponent
var walletId = new WalletId(store.Id, CryptoCode);
var derivation = store.GetDerivationSchemeSettings(_networkProvider, walletId.CryptoCode);
var transactionsCount = 0;
if (derivation != null)
int? transactionsCount = null;
if (derivation != null && _nbxConnectionFactory.Available)
{
var network = derivation.Network;
var wallet = _walletProvider.GetWallet(network);
var allTransactions = await wallet.FetchTransactions(derivation.AccountDerivation);
await using var conn = await _nbxConnectionFactory.OpenConnection();
var wid = NBXplorer.Client.DBUtils.nbxv1_get_wallet_id(derivation.Network.CryptoCode, derivation.AccountDerivation.ToString());
var afterDate = DateTimeOffset.UtcNow - TimeSpan.FromDays(TransactionDays);
transactionsCount = allTransactions.UnconfirmedTransactions.Transactions
.Concat(allTransactions.ConfirmedTransactions.Transactions)
.Count(t => t.Timestamp > afterDate);
var count = await conn.ExecuteScalarAsync<long>("SELECT COUNT(*) FROM wallets_history WHERE code=@code AND wallet_id=@wid AND seen_at > @afterDate", new { code = derivation.Network.CryptoCode, wid, afterDate });
transactionsCount = (int)count;
}
var vm = new StoreNumbersViewModel

View File

@ -8,7 +8,7 @@ public class StoreNumbersViewModel
public StoreData Store { get; set; }
public WalletId WalletId { get; set; }
public int PayoutsPending { get; set; }
public int Transactions { get; set; }
public int? Transactions { get; set; }
public int RefundsIssued { get; set; }
public int TransactionDays { get; set; }
}

View File

@ -21,7 +21,7 @@ namespace BTCPayServer.Components.StoreRecentTransactions;
public class StoreRecentTransactions : ViewComponent
{
private const string CryptoCode = "BTC";
private string CryptoCode;
private readonly StoreRepository _storeRepo;
private readonly ApplicationDbContextFactory _dbContextFactory;
private readonly BTCPayWalletProvider _walletProvider;
@ -41,6 +41,7 @@ public class StoreRecentTransactions : ViewComponent
ConnectionFactory = connectionFactory;
_walletProvider = walletProvider;
_dbContextFactory = dbContextFactory;
CryptoCode = networkProvider.DefaultNetwork.CryptoCode;
}

View File

@ -4,11 +4,14 @@
<div id="StoreWalletBalance-@Model.Store.Id" class="widget store-wallet-balance">
<h6 class="mb-2">Wallet Balance</h6>
<header class="mb-3">
<div class="balance">
<h3 class="d-inline-block me-1">@Model.Balance</h3>
<span class="text-secondary fw-semibold">@Model.CryptoCode</span>
</div>
<div class="btn-group mt-1" role="group" aria-label="Filter">
@if (Model.Balance is not null)
{
<div class="balance">
<h3 class="d-inline-block me-1">@Model.Balance</h3>
<span class="text-secondary fw-semibold">@Model.CryptoCode</span>
</div>
}
<div class="btn-group mt-1" role="group" aria-label="Filter">
<input type="radio" class="btn-check" name="filter" id="filter-week" value="week" @(Model.Type == WalletHistogramType.Week ? "checked" : "")>
<label class="btn btn-link" for="filter-week">1W</label>
<input type="radio" class="btn-check" name="filter" id="filter-month" value="month" @(Model.Type == WalletHistogramType.Month ? "checked" : "")>
@ -24,11 +27,12 @@
const baseUrl = @Safe.Json(Url.Action("WalletHistogram", "UIWallets", new { walletId = Model.WalletId, type = WalletHistogramType.Week }));
const render = data => {
const { series, labels, balance } = data;
document.querySelector(`#${id} h3`).innerText = balance;
if (balance)
document.querySelector(`#${id} h3`).innerText = balance;
const min = Math.min(...series);
const max = Math.max(...series);
const low = Math.max(min - ((max - min) / 5), 0);
new Chartist.Line(`#${id} .ct-chart`, {
const chart = new Chartist.Line(`#${id} .ct-chart`, {
labels,
series: [series]
}, {
@ -36,6 +40,14 @@
fullWidth: true,
showArea: true
});
// prevent y-axis labels from getting cut off
window.setTimeout(() => {
const yLabels = [...document.querySelectorAll('.ct-label.ct-vertical.ct-start')];
if (yLabels) {
const width = Math.max(...(yLabels.map(l => l.innerText.length * 8)))
chart.update(null, { axisY: { offset: width } })
}
}, 0)
};
const update = async type => {
const url = baseUrl.replace(/\/week$/gi, `/${type}`);

View File

@ -18,16 +18,17 @@ namespace BTCPayServer.Components.StoreWalletBalance;
public class StoreWalletBalance : ViewComponent
{
private const string CryptoCode = "BTC";
private string CryptoCode;
private const WalletHistogramType DefaultType = WalletHistogramType.Week;
private readonly StoreRepository _storeRepo;
private readonly WalletHistogramService _walletHistogramService;
public StoreWalletBalance(StoreRepository storeRepo, WalletHistogramService walletHistogramService)
public StoreWalletBalance(StoreRepository storeRepo, WalletHistogramService walletHistogramService, BTCPayNetworkProvider networkProvider)
{
_storeRepo = storeRepo;
_walletHistogramService = walletHistogramService;
CryptoCode = networkProvider.DefaultNetwork.CryptoCode;
}
public async Task<IViewComponentResult> InvokeAsync(StoreData store)
@ -42,7 +43,7 @@ public class StoreWalletBalance : ViewComponent
WalletId = walletId,
Series = data?.Series,
Labels = data?.Labels,
Balance = data?.Balance ?? 0,
Balance = data?.Balance,
Type = DefaultType
};

View File

@ -6,7 +6,7 @@ namespace BTCPayServer.Components.StoreWalletBalance;
public class StoreWalletBalanceViewModel
{
public decimal Balance { get; set; }
public decimal? Balance { get; set; }
public string CryptoCode { get; set; }
public StoreData Store { get; set; }
public WalletId WalletId { get; set; }

View File

@ -7,6 +7,5 @@ namespace BTCPayServer.Configuration
{
public Dictionary<string, Uri> OtherExternalServices { get; set; } = new Dictionary<string, Uri>();
public ExternalServices ExternalServices { get; set; } = new ExternalServices();
}
}

View File

@ -1,3 +1,4 @@
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Contracts;
@ -40,9 +41,9 @@ namespace BTCPayServer.Controllers.Greenfield
[Authorize(Policy = Policies.CanUseInternalLightningNode,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/server/lightning/{cryptoCode}/info")]
public override Task<IActionResult> GetInfo(string cryptoCode)
public override Task<IActionResult> GetInfo(string cryptoCode, CancellationToken cancellationToken = default)
{
return base.GetInfo(cryptoCode);
return base.GetInfo(cryptoCode, cancellationToken);
}
[Authorize(Policy = Policies.CanUseInternalLightningNode,
@ -56,17 +57,17 @@ namespace BTCPayServer.Controllers.Greenfield
[Authorize(Policy = Policies.CanUseInternalLightningNode,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/server/lightning/{cryptoCode}/channels")]
public override Task<IActionResult> GetChannels(string cryptoCode)
public override Task<IActionResult> GetChannels(string cryptoCode, CancellationToken cancellationToken = default)
{
return base.GetChannels(cryptoCode);
return base.GetChannels(cryptoCode, cancellationToken);
}
[Authorize(Policy = Policies.CanUseInternalLightningNode,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpPost("~/api/v1/server/lightning/{cryptoCode}/channels")]
public override Task<IActionResult> OpenChannel(string cryptoCode, OpenLightningChannelRequest request)
public override Task<IActionResult> OpenChannel(string cryptoCode, OpenLightningChannelRequest request, CancellationToken cancellationToken = default)
{
return base.OpenChannel(cryptoCode, request);
return base.OpenChannel(cryptoCode, request, cancellationToken);
}
[Authorize(Policy = Policies.CanUseInternalLightningNode,
@ -80,33 +81,33 @@ namespace BTCPayServer.Controllers.Greenfield
[Authorize(Policy = Policies.CanUseInternalLightningNode,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/server/lightning/{cryptoCode}/payments/{paymentHash}")]
public override Task<IActionResult> GetPayment(string cryptoCode, string paymentHash)
public override Task<IActionResult> GetPayment(string cryptoCode, string paymentHash, CancellationToken cancellationToken = default)
{
return base.GetPayment(cryptoCode, paymentHash);
return base.GetPayment(cryptoCode, paymentHash, cancellationToken);
}
[Authorize(Policy = Policies.CanUseInternalLightningNode,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/server/lightning/{cryptoCode}/invoices/{id}")]
public override Task<IActionResult> GetInvoice(string cryptoCode, string id)
public override Task<IActionResult> GetInvoice(string cryptoCode, string id, CancellationToken cancellationToken = default)
{
return base.GetInvoice(cryptoCode, id);
return base.GetInvoice(cryptoCode, id, cancellationToken);
}
[Authorize(Policy = Policies.CanUseInternalLightningNode,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpPost("~/api/v1/server/lightning/{cryptoCode}/invoices/pay")]
public override Task<IActionResult> PayInvoice(string cryptoCode, PayLightningInvoiceRequest lightningInvoice)
public override Task<IActionResult> PayInvoice(string cryptoCode, PayLightningInvoiceRequest lightningInvoice, CancellationToken cancellationToken = default)
{
return base.PayInvoice(cryptoCode, lightningInvoice);
return base.PayInvoice(cryptoCode, lightningInvoice, cancellationToken);
}
[Authorize(Policy = Policies.CanCreateLightningInvoiceInternalNode,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpPost("~/api/v1/server/lightning/{cryptoCode}/invoices")]
public override Task<IActionResult> CreateInvoice(string cryptoCode, CreateLightningInvoiceRequest request)
public override Task<IActionResult> CreateInvoice(string cryptoCode, CreateLightningInvoiceRequest request, CancellationToken cancellationToken = default)
{
return base.CreateInvoice(cryptoCode, request);
return base.CreateInvoice(cryptoCode, request, cancellationToken);
}
protected override async Task<ILightningClient> GetLightningClient(string cryptoCode, bool doingAdminThings)

View File

@ -1,16 +1,16 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Configuration;
using BTCPayServer.Data;
using BTCPayServer.HostedServices;
using BTCPayServer.Lightning;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Security;
using BTCPayServer.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
@ -43,9 +43,9 @@ namespace BTCPayServer.Controllers.Greenfield
[Authorize(Policy = Policies.CanUseLightningNodeInStore,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/info")]
public override Task<IActionResult> GetInfo(string cryptoCode)
public override Task<IActionResult> GetInfo(string cryptoCode, CancellationToken cancellationToken = default)
{
return base.GetInfo(cryptoCode);
return base.GetInfo(cryptoCode, cancellationToken);
}
[Authorize(Policy = Policies.CanUseLightningNodeInStore,
@ -58,16 +58,16 @@ namespace BTCPayServer.Controllers.Greenfield
[Authorize(Policy = Policies.CanUseLightningNodeInStore,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/channels")]
public override Task<IActionResult> GetChannels(string cryptoCode)
public override Task<IActionResult> GetChannels(string cryptoCode, CancellationToken cancellationToken = default)
{
return base.GetChannels(cryptoCode);
return base.GetChannels(cryptoCode, cancellationToken);
}
[Authorize(Policy = Policies.CanUseLightningNodeInStore,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpPost("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/channels")]
public override Task<IActionResult> OpenChannel(string cryptoCode, OpenLightningChannelRequest request)
public override Task<IActionResult> OpenChannel(string cryptoCode, OpenLightningChannelRequest request, CancellationToken cancellationToken = default)
{
return base.OpenChannel(cryptoCode, request);
return base.OpenChannel(cryptoCode, request, cancellationToken);
}
[Authorize(Policy = Policies.CanUseLightningNodeInStore,
@ -81,46 +81,50 @@ namespace BTCPayServer.Controllers.Greenfield
[Authorize(Policy = Policies.CanUseLightningNodeInStore,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/payments/{paymentHash}")]
public override Task<IActionResult> GetPayment(string cryptoCode, string paymentHash)
public override Task<IActionResult> GetPayment(string cryptoCode, string paymentHash, CancellationToken cancellationToken = default)
{
return base.GetPayment(cryptoCode, paymentHash);
return base.GetPayment(cryptoCode, paymentHash, cancellationToken);
}
[Authorize(Policy = Policies.CanUseLightningNodeInStore,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpPost("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/invoices/pay")]
public override Task<IActionResult> PayInvoice(string cryptoCode, PayLightningInvoiceRequest lightningInvoice)
public override Task<IActionResult> PayInvoice(string cryptoCode, PayLightningInvoiceRequest lightningInvoice, CancellationToken cancellationToken = default)
{
return base.PayInvoice(cryptoCode, lightningInvoice);
return base.PayInvoice(cryptoCode, lightningInvoice, cancellationToken);
}
[Authorize(Policy = Policies.CanUseLightningNodeInStore,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/invoices/{id}")]
public override Task<IActionResult> GetInvoice(string cryptoCode, string id)
public override Task<IActionResult> GetInvoice(string cryptoCode, string id, CancellationToken cancellationToken = default)
{
return base.GetInvoice(cryptoCode, id);
return base.GetInvoice(cryptoCode, id, cancellationToken);
}
[Authorize(Policy = Policies.CanCreateLightningInvoiceInStore,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpPost("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/invoices")]
public override Task<IActionResult> CreateInvoice(string cryptoCode, CreateLightningInvoiceRequest request)
public override Task<IActionResult> CreateInvoice(string cryptoCode, CreateLightningInvoiceRequest request, CancellationToken cancellationToken = default)
{
return base.CreateInvoice(cryptoCode, request);
return base.CreateInvoice(cryptoCode, request, cancellationToken);
}
protected override Task<ILightningClient> GetLightningClient(string cryptoCode,
bool doingAdminThings)
{
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
var store = HttpContext.GetStoreData();
if (store == null || network == null)
if (network == null)
{
throw ErrorCryptoCodeNotFound();
}
var store = HttpContext.GetStoreData();
if (store == null)
{
throw new JsonHttpException(StoreNotFound());
}
var id = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike);
var existing = store.GetSupportedPaymentMethods(_btcPayNetworkProvider)
.OfType<LightningSupportedPaymentMethod>()
@ -143,5 +147,10 @@ namespace BTCPayServer.Controllers.Greenfield
}
throw ErrorLightningNodeNotConfiguredForStore();
}
private IActionResult StoreNotFound()
{
return this.CreateAPIError(404, "store-not-found", "The store was not found");
}
}
}

View File

@ -39,11 +39,11 @@ namespace BTCPayServer.Controllers.Greenfield
_authorizationService = authorizationService;
}
public virtual async Task<IActionResult> GetInfo(string cryptoCode)
public virtual async Task<IActionResult> GetInfo(string cryptoCode, CancellationToken cancellationToken = default)
{
var lightningClient = await GetLightningClient(cryptoCode, true);
var info = await lightningClient.GetInfo();
return Ok(new LightningNodeInformationData()
var info = await lightningClient.GetInfo(cancellationToken);
return Ok(new LightningNodeInformationData
{
BlockHeight = info.BlockHeight,
NodeURIs = info.NodeInfoList.Select(nodeInfo => nodeInfo).ToArray()
@ -75,12 +75,12 @@ namespace BTCPayServer.Controllers.Greenfield
return Ok();
}
public virtual async Task<IActionResult> GetChannels(string cryptoCode)
public virtual async Task<IActionResult> GetChannels(string cryptoCode, CancellationToken cancellationToken = default)
{
var lightningClient = await GetLightningClient(cryptoCode, true);
var channels = await lightningClient.ListChannels();
return Ok(channels.Select(channel => new LightningChannelData()
var channels = await lightningClient.ListChannels(cancellationToken);
return Ok(channels.Select(channel => new LightningChannelData
{
Capacity = channel.Capacity,
ChannelPoint = channel.ChannelPoint.ToString(),
@ -92,7 +92,7 @@ namespace BTCPayServer.Controllers.Greenfield
}
public virtual async Task<IActionResult> OpenChannel(string cryptoCode, OpenLightningChannelRequest request)
public virtual async Task<IActionResult> OpenChannel(string cryptoCode, OpenLightningChannelRequest request, CancellationToken cancellationToken = default)
{
var lightningClient = await GetLightningClient(cryptoCode, true);
if (request?.NodeURI is null)
@ -124,12 +124,12 @@ namespace BTCPayServer.Controllers.Greenfield
return this.CreateValidationError(ModelState);
}
var response = await lightningClient.OpenChannel(new Lightning.OpenChannelRequest()
var response = await lightningClient.OpenChannel(new OpenChannelRequest
{
ChannelAmount = request.ChannelAmount,
FeeRate = request.FeeRate,
NodeInfo = request.NodeURI
});
}, cancellationToken);
string errorCode, errorMessage;
switch (response.Result)
@ -164,14 +164,14 @@ namespace BTCPayServer.Controllers.Greenfield
return Ok(new JValue((await lightningClient.GetDepositAddress()).ToString()));
}
public virtual async Task<IActionResult> GetPayment(string cryptoCode, string paymentHash)
public virtual async Task<IActionResult> GetPayment(string cryptoCode, string paymentHash, CancellationToken cancellationToken = default)
{
var lightningClient = await GetLightningClient(cryptoCode, false);
var payment = await lightningClient.GetPayment(paymentHash);
var payment = await lightningClient.GetPayment(paymentHash, cancellationToken);
return payment == null ? this.CreateAPIError(404, "payment-not-found", "Impossible to find a lightning payment with this payment hash") : Ok(ToModel(payment));
}
public virtual async Task<IActionResult> PayInvoice(string cryptoCode, PayLightningInvoiceRequest lightningInvoice)
public virtual async Task<IActionResult> PayInvoice(string cryptoCode, PayLightningInvoiceRequest lightningInvoice, CancellationToken cancellationToken = default)
{
var lightningClient = await GetLightningClient(cryptoCode, true);
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
@ -190,7 +190,7 @@ namespace BTCPayServer.Controllers.Greenfield
var param = lightningInvoice?.MaxFeeFlat != null || lightningInvoice?.MaxFeePercent != null
? new PayInvoiceParams { MaxFeePercent = lightningInvoice.MaxFeePercent, MaxFeeFlat = lightningInvoice.MaxFeeFlat }
: null;
var result = await lightningClient.Pay(lightningInvoice.BOLT11, param);
var result = await lightningClient.Pay(lightningInvoice.BOLT11, param, cancellationToken);
return result.Result switch
{
@ -205,14 +205,14 @@ namespace BTCPayServer.Controllers.Greenfield
};
}
public virtual async Task<IActionResult> GetInvoice(string cryptoCode, string id)
public virtual async Task<IActionResult> GetInvoice(string cryptoCode, string id, CancellationToken cancellationToken = default)
{
var lightningClient = await GetLightningClient(cryptoCode, false);
var inv = await lightningClient.GetInvoice(id);
var inv = await lightningClient.GetInvoice(id, cancellationToken);
return inv == null ? this.CreateAPIError(404, "invoice-not-found", "Impossible to find a lightning invoice with this id") : Ok(ToModel(inv));
}
public virtual async Task<IActionResult> CreateInvoice(string cryptoCode, CreateLightningInvoiceRequest request)
public virtual async Task<IActionResult> CreateInvoice(string cryptoCode, CreateLightningInvoiceRequest request, CancellationToken cancellationToken = default)
{
var lightningClient = await GetLightningClient(cryptoCode, false);
if (request.Amount < LightMoney.Zero)
@ -241,7 +241,7 @@ namespace BTCPayServer.Controllers.Greenfield
{
PrivateRouteHints = request.PrivateRouteHints, DescriptionHash = request.DescriptionHash
};
var invoice = await lightningClient.CreateInvoice(param, CancellationToken.None);
var invoice = await lightningClient.CreateInvoice(param, cancellationToken);
return Ok(ToModel(invoice));
}
catch (Exception ex)

View File

@ -38,7 +38,7 @@ namespace BTCPayServer.Controllers.Greenfield
{
var prs = await _paymentRequestRepository.FindPaymentRequests(
new PaymentRequestQuery() { StoreId = storeId, IncludeArchived = includeArchived });
return Ok(prs.Items.Select(FromModel));
return Ok(prs.Select(FromModel));
}
[Authorize(Policy = Policies.CanViewPaymentRequests, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
@ -48,12 +48,12 @@ namespace BTCPayServer.Controllers.Greenfield
var pr = await _paymentRequestRepository.FindPaymentRequests(
new PaymentRequestQuery() { StoreId = storeId, Ids = new[] { paymentRequestId } });
if (pr.Total == 0)
if (pr.Length == 0)
{
return PaymentRequestNotFound();
}
return Ok(FromModel(pr.Items.First()));
return Ok(FromModel(pr.First()));
}
[Authorize(Policy = Policies.CanModifyPaymentRequests,
@ -63,12 +63,12 @@ namespace BTCPayServer.Controllers.Greenfield
{
var pr = await _paymentRequestRepository.FindPaymentRequests(
new PaymentRequestQuery() { StoreId = storeId, Ids = new[] { paymentRequestId }, IncludeArchived = false });
if (pr.Total == 0)
if (pr.Length == 0)
{
return PaymentRequestNotFound();
}
var updatedPr = pr.Items.First();
var updatedPr = pr.First();
updatedPr.Archived = true;
await _paymentRequestRepository.CreateOrUpdatePaymentRequest(updatedPr);
return Ok();
@ -111,12 +111,12 @@ namespace BTCPayServer.Controllers.Greenfield
request.Currency ??= StoreData.GetStoreBlob().DefaultCurrency;
var pr = await _paymentRequestRepository.FindPaymentRequests(
new PaymentRequestQuery() { StoreId = storeId, Ids = new[] { paymentRequestId } });
if (pr.Total == 0)
if (pr.Length == 0)
{
return PaymentRequestNotFound();
}
var updatedPr = pr.Items.First();
var updatedPr = pr.First();
updatedPr.SetBlob(request);
return Ok(FromModel(await _paymentRequestRepository.CreateOrUpdatePaymentRequest(updatedPr)));

View File

@ -0,0 +1,43 @@
#nullable enable
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.PayoutProcessors;
using BTCPayServer.Security;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using StoreData = BTCPayServer.Data.StoreData;
namespace BTCPayServer.Controllers.Greenfield
{
[ApiController]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public class GreenfieldPayoutProcessorsController : ControllerBase
{
private readonly IEnumerable<IPayoutProcessorFactory> _factories;
public GreenfieldPayoutProcessorsController(IEnumerable<IPayoutProcessorFactory>factories)
{
_factories = factories;
}
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/payout-processors")]
public IActionResult GetPayoutProcessors()
{
return Ok(_factories.Select(factory => new PayoutProcessorData()
{
Name = factory.Processor,
FriendlyName = factory.FriendlyName,
PaymentMethods = factory.GetSupportedPaymentMethods().Select(id => id.ToStringNormalized())
.ToArray()
}));
}
}
}

View File

@ -31,7 +31,6 @@ namespace BTCPayServer.Controllers.Greenfield
private readonly ApplicationDbContextFactory _dbContextFactory;
private readonly CurrencyNameTable _currencyNameTable;
private readonly BTCPayNetworkJsonSerializerSettings _serializerSettings;
private readonly BTCPayNetworkProvider _networkProvider;
private readonly IEnumerable<IPayoutHandler> _payoutHandlers;
public GreenfieldPullPaymentController(PullPaymentHostedService pullPaymentService,
@ -39,7 +38,6 @@ namespace BTCPayServer.Controllers.Greenfield
ApplicationDbContextFactory dbContextFactory,
CurrencyNameTable currencyNameTable,
Services.BTCPayNetworkJsonSerializerSettings serializerSettings,
BTCPayNetworkProvider networkProvider,
IEnumerable<IPayoutHandler> payoutHandlers)
{
_pullPaymentService = pullPaymentService;
@ -47,7 +45,6 @@ namespace BTCPayServer.Controllers.Greenfield
_dbContextFactory = dbContextFactory;
_currencyNameTable = currencyNameTable;
_serializerSettings = serializerSettings;
_networkProvider = networkProvider;
_payoutHandlers = payoutHandlers;
}
@ -138,7 +135,8 @@ namespace BTCPayServer.Controllers.Greenfield
Amount = request.Amount,
Currency = request.Currency,
StoreId = storeId,
PaymentMethodIds = paymentMethods
PaymentMethodIds = paymentMethods,
AutoApproveClaims = request.AutoApproveClaims
});
var pp = await _pullPaymentService.GetPullPayment(ppId, false);
return this.Ok(CreatePullPaymentData(pp));
@ -158,6 +156,7 @@ namespace BTCPayServer.Controllers.Greenfield
Currency = ppBlob.Currency,
Period = ppBlob.Period,
Archived = pp.Archived,
AutoApproveClaims = ppBlob.AutoApproveClaims,
BOLT11Expiration = ppBlob.BOLT11Expiration,
ViewLink = _linkGenerator.GetUriByAction(
nameof(UIPullPaymentController.ViewPullPayment),
@ -191,9 +190,8 @@ namespace BTCPayServer.Controllers.Greenfield
if (pp is null)
return PullPaymentNotFound();
var payouts = pp.Payouts.Where(p => p.State != PayoutState.Cancelled || includeCancelled).ToList();
var cd = _currencyNameTable.GetCurrencyData(pp.GetBlob().Currency, false);
return base.Ok(payouts
.Select(p => ToModel(p, cd)).ToList());
.Select(ToModel).ToList());
}
[HttpGet("~/api/v1/pull-payments/{pullPaymentId}/payouts/{payoutId}")]
@ -209,11 +207,10 @@ namespace BTCPayServer.Controllers.Greenfield
var payout = pp.Payouts.FirstOrDefault(p => p.Id == payoutId);
if (payout is null)
return PayoutNotFound();
var cd = _currencyNameTable.GetCurrencyData(payout.PullPaymentData.GetBlob().Currency, false);
return base.Ok(ToModel(payout, cd));
return base.Ok(ToModel(payout));
}
private Client.Models.PayoutData ToModel(Data.PayoutData p, CurrencyData cd)
private Client.Models.PayoutData ToModel(Data.PayoutData p)
{
var blob = p.GetBlob(_serializerSettings);
var model = new Client.Models.PayoutData()
@ -275,14 +272,83 @@ namespace BTCPayServer.Controllers.Greenfield
ModelState.AddModelError(nameof(request.Amount), $"Amount too small (should be at least {ppBlob.MinimumClaim})");
return this.CreateValidationError(ModelState);
}
var cd = _currencyNameTable.GetCurrencyData(pp.GetBlob().Currency, false);
var result = await _pullPaymentService.Claim(new ClaimRequest()
var result = await _pullPaymentService.Claim(new ClaimRequest()
{
Destination = destination.destination,
PullPaymentId = pullPaymentId,
Value = request.Amount,
PaymentMethodId = paymentMethodId
PaymentMethodId = paymentMethodId,
});
return HandleClaimResult(result);
}
[HttpPost("~/api/v1/stores/{storeId}/payouts")]
[Authorize(Policy = Policies.CanManagePullPayments, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public async Task<IActionResult> CreatePayoutThroughStore(string storeId, CreatePayoutThroughStoreRequest request)
{
if (request is null || !PaymentMethodId.TryParse(request?.PaymentMethod, out var paymentMethodId))
{
ModelState.AddModelError(nameof(request.PaymentMethod), "Invalid payment method");
return this.CreateValidationError(ModelState);
}
var payoutHandler = _payoutHandlers.FindPayoutHandler(paymentMethodId);
if (payoutHandler is null)
{
ModelState.AddModelError(nameof(request.PaymentMethod), "Invalid payment method");
return this.CreateValidationError(ModelState);
}
await using var ctx = _dbContextFactory.CreateContext();
PullPaymentBlob? ppBlob = null;
if (request?.PullPaymentId is not null)
{
var pp = await ctx.PullPayments.FirstOrDefaultAsync(data =>
data.Id == request.PullPaymentId && data.StoreId == storeId);
if (pp is null)
return PullPaymentNotFound();
ppBlob = pp.GetBlob();
}
var destination = await payoutHandler.ParseAndValidateClaimDestination(paymentMethodId, request!.Destination, ppBlob);
if (destination.destination is null)
{
ModelState.AddModelError(nameof(request.Destination), destination.error ?? "The destination is invalid for the payment specified");
return this.CreateValidationError(ModelState);
}
if (request.Amount is null && destination.destination.Amount != null)
{
request.Amount = destination.destination.Amount;
}
else if (request.Amount != null && destination.destination.Amount != null && request.Amount != destination.destination.Amount)
{
ModelState.AddModelError(nameof(request.Amount), $"Amount is implied in destination ({destination.destination.Amount}) that does not match the payout amount provided {request.Amount})");
return this.CreateValidationError(ModelState);
}
if (request.Amount is { } v && (v < ppBlob?.MinimumClaim || v == 0.0m))
{
var minimumClaim = ppBlob?.MinimumClaim is decimal val ? val : 0.0m;
ModelState.AddModelError(nameof(request.Amount), $"Amount too small (should be at least {minimumClaim})");
return this.CreateValidationError(ModelState);
}
var result = await _pullPaymentService.Claim(new ClaimRequest()
{
Destination = destination.destination,
PullPaymentId = request.PullPaymentId,
PreApprove = request.Approved,
Value = request.Amount,
PaymentMethodId = paymentMethodId,
StoreId = storeId
});
return HandleClaimResult(result);
}
private IActionResult HandleClaimResult(ClaimRequest.ClaimResponse result)
{
switch (result.Result)
{
case ClaimRequest.ClaimResult.Ok:
@ -304,7 +370,8 @@ namespace BTCPayServer.Controllers.Greenfield
default:
throw new NotSupportedException("Unsupported ClaimResult");
}
return Ok(ToModel(result.PayoutData, cd));
return Ok(ToModel(result.PayoutData));
}
[HttpDelete("~/api/v1/stores/{storeId}/pull-payments/{pullPaymentId}")]
@ -319,6 +386,20 @@ namespace BTCPayServer.Controllers.Greenfield
return Ok();
}
[HttpGet("~/api/v1/stores/{storeId}/payouts")]
[Authorize(Policy = Policies.CanManagePullPayments, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public async Task<IActionResult> GetStorePayouts(string storeId, bool includeCancelled = false)
{
await using var ctx = _dbContextFactory.CreateContext();
var payouts = await ctx.Payouts
.Where(p => p.StoreDataId == storeId && (p.State != PayoutState.Cancelled || includeCancelled))
.ToListAsync();
return base.Ok(payouts
.Select(ToModel).ToList());
}
[HttpDelete("~/api/v1/stores/{storeId}/payouts/{payoutId}")]
[Authorize(Policy = Policies.CanManagePullPayments, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public async Task<IActionResult> CancelPayout(string storeId, string payoutId)
@ -361,8 +442,6 @@ namespace BTCPayServer.Controllers.Greenfield
ModelState.AddModelError(nameof(approvePayoutRequest.RateRule), "Invalid RateRule");
return this.CreateValidationError(ModelState);
}
var ppBlob = payout.PullPaymentData.GetBlob();
var cd = _currencyNameTable.GetCurrencyData(ppBlob.Currency, false);
var result = await _pullPaymentService.Approve(new PullPaymentHostedService.PayoutApproval()
{
PayoutId = payoutId,
@ -373,7 +452,7 @@ namespace BTCPayServer.Controllers.Greenfield
switch (result)
{
case PullPaymentHostedService.PayoutApproval.Result.Ok:
return Ok(ToModel(await ctx.Payouts.GetPayout(payoutId, storeId, true), cd));
return Ok(ToModel(await ctx.Payouts.GetPayout(payoutId, storeId, true)));
case PullPaymentHostedService.PayoutApproval.Result.InvalidState:
return this.CreateAPIError("invalid-state", errorMessage);
case PullPaymentHostedService.PayoutApproval.Result.TooLowAmount:

View File

@ -0,0 +1,94 @@
#nullable enable
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Data.Data;
using BTCPayServer.PayoutProcessors;
using BTCPayServer.PayoutProcessors.Lightning;
using BTCPayServer.PayoutProcessors.Settings;
using BTCPayServer.Services.Invoices;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using PayoutProcessorData = BTCPayServer.Data.Data.PayoutProcessorData;
namespace BTCPayServer.Controllers.Greenfield
{
[ApiController]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public class GreenfieldStoreAutomatedLightningPayoutProcessorsController : ControllerBase
{
private readonly PayoutProcessorService _payoutProcessorService;
private readonly EventAggregator _eventAggregator;
public GreenfieldStoreAutomatedLightningPayoutProcessorsController(PayoutProcessorService payoutProcessorService,
EventAggregator eventAggregator)
{
_payoutProcessorService = payoutProcessorService;
_eventAggregator = eventAggregator;
}
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}/payout-processors/" + nameof(LightningAutomatedPayoutSenderFactory))]
[HttpGet("~/api/v1/stores/{storeId}/payout-processors/" + nameof(LightningAutomatedPayoutSenderFactory) +
"/{paymentMethod}")]
public async Task<IActionResult> GetStoreLightningAutomatedPayoutProcessors(
string storeId, string? paymentMethod)
{
var configured =
await _payoutProcessorService.GetProcessors(
new PayoutProcessorService.PayoutProcessorQuery()
{
Stores = new[] {storeId},
Processors = new[] {LightningAutomatedPayoutSenderFactory.ProcessorName},
PaymentMethods = paymentMethod is null ? null : new[] {paymentMethod}
});
return Ok(configured.Select(ToModel).ToArray());
}
private static LightningAutomatedPayoutSettings ToModel(PayoutProcessorData data)
{
return new LightningAutomatedPayoutSettings()
{
PaymentMethod = data.PaymentMethod,
IntervalSeconds = InvoiceRepository.FromBytes<AutomatedPayoutBlob>(data.Blob).Interval
};
}
private static AutomatedPayoutBlob FromModel(LightningAutomatedPayoutSettings data)
{
return new AutomatedPayoutBlob() {Interval = data.IntervalSeconds};
}
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpPut("~/api/v1/stores/{storeId}/payout-processors/" + nameof(LightningAutomatedPayoutSenderFactory) +
"/{paymentMethod}")]
public async Task<IActionResult> UpdateStoreLightningAutomatedPayoutProcessor(
string storeId, string paymentMethod, LightningAutomatedPayoutSettings request)
{
var activeProcessor =
(await _payoutProcessorService.GetProcessors(
new PayoutProcessorService.PayoutProcessorQuery()
{
Stores = new[] {storeId},
Processors = new[] {LightningAutomatedPayoutSenderFactory.ProcessorName},
PaymentMethods = new[] {paymentMethod}
}))
.FirstOrDefault();
activeProcessor ??= new PayoutProcessorData();
activeProcessor.Blob = InvoiceRepository.ToBytes(FromModel(request));
activeProcessor.StoreId = storeId;
activeProcessor.PaymentMethod = paymentMethod;
activeProcessor.Processor = LightningAutomatedPayoutSenderFactory.ProcessorName;
var tcs = new TaskCompletionSource();
_eventAggregator.Publish(new PayoutProcessorUpdated()
{
Data = activeProcessor, Id = activeProcessor.Id, Processed = tcs
});
await tcs.Task;
return Ok(ToModel(activeProcessor));
}
}
}

View File

@ -0,0 +1,95 @@
#nullable enable
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Data.Data;
using BTCPayServer.PayoutProcessors;
using BTCPayServer.PayoutProcessors.OnChain;
using BTCPayServer.PayoutProcessors.Settings;
using BTCPayServer.Services.Invoices;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using PayoutProcessorData = BTCPayServer.Data.Data.PayoutProcessorData;
namespace BTCPayServer.Controllers.Greenfield
{
[ApiController]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public class GreenfieldStoreAutomatedOnChainPayoutProcessorsController : ControllerBase
{
private readonly PayoutProcessorService _payoutProcessorService;
private readonly EventAggregator _eventAggregator;
public GreenfieldStoreAutomatedOnChainPayoutProcessorsController(PayoutProcessorService payoutProcessorService,
EventAggregator eventAggregator)
{
_payoutProcessorService = payoutProcessorService;
_eventAggregator = eventAggregator;
}
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}/payout-processors/" + nameof(OnChainAutomatedPayoutSenderFactory))]
[HttpGet("~/api/v1/stores/{storeId}/payout-processors/" + nameof(OnChainAutomatedPayoutSenderFactory) +
"/{paymentMethod}")]
public async Task<IActionResult> GetStoreOnChainAutomatedPayoutProcessors(
string storeId, string? paymentMethod)
{
var configured =
await _payoutProcessorService.GetProcessors(
new PayoutProcessorService.PayoutProcessorQuery()
{
Stores = new[] {storeId},
Processors = new[] {OnChainAutomatedPayoutSenderFactory.ProcessorName},
PaymentMethods = paymentMethod is null ? null : new[] {paymentMethod}
});
return Ok(configured.Select(ToModel).ToArray());
}
private static OnChainAutomatedPayoutSettings ToModel(PayoutProcessorData data)
{
return new OnChainAutomatedPayoutSettings()
{
PaymentMethod = data.PaymentMethod,
IntervalSeconds = InvoiceRepository.FromBytes<AutomatedPayoutBlob>(data.Blob).Interval
};
}
private static AutomatedPayoutBlob FromModel(OnChainAutomatedPayoutSettings data)
{
return new AutomatedPayoutBlob() {Interval = data.IntervalSeconds};
}
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpPut("~/api/v1/stores/{storeId}/payout-processors/" + nameof(OnChainAutomatedPayoutSenderFactory) +
"/{paymentMethod}")]
public async Task<IActionResult> UpdateStoreOnchainAutomatedPayoutProcessor(
string storeId, string paymentMethod, OnChainAutomatedPayoutSettings request)
{
var activeProcessor =
(await _payoutProcessorService.GetProcessors(
new PayoutProcessorService.PayoutProcessorQuery()
{
Stores = new[] {storeId},
Processors = new[] {OnChainAutomatedPayoutSenderFactory.ProcessorName},
PaymentMethods = new[] {paymentMethod}
}))
.FirstOrDefault();
activeProcessor ??= new PayoutProcessorData();
activeProcessor.Blob = InvoiceRepository.ToBytes(FromModel(request));
activeProcessor.StoreId = storeId;
activeProcessor.PaymentMethod = paymentMethod;
activeProcessor.Processor = OnChainAutomatedPayoutSenderFactory.ProcessorName;
var tcs = new TaskCompletionSource();
_eventAggregator.Publish(new PayoutProcessorUpdated()
{
Data = activeProcessor, Id = activeProcessor.Id, Processed = tcs
});
await tcs.Task;
return Ok(ToModel(activeProcessor));
}
}
}

View File

@ -173,6 +173,7 @@ namespace BTCPayServer.Controllers.Greenfield
string storeId,
string cryptoCode,
[FromQuery] TransactionStatus[]? statusFilter = null,
[FromQuery] string? labelFilter = null,
[FromQuery] int skip = 0,
[FromQuery] int limit = int.MaxValue
)
@ -202,6 +203,23 @@ namespace BTCPayServer.Controllers.Greenfield
filteredFlatList.AddRange(txs.ReplacedTransactions.Transactions);
}
if (labelFilter != null)
{
filteredFlatList = filteredFlatList.Where(information =>
{
walletTransactionsInfoAsync.TryGetValue(information.TransactionId.ToString(), out var transactionInfo);
if (transactionInfo != null)
{
return transactionInfo.Labels.ContainsKey(labelFilter);
}
else
{
return false;
}
}).ToList();
}
var result = filteredFlatList.Skip(skip).Take(limit).Select(information =>
{
walletTransactionsInfoAsync.TryGetValue(information.TransactionId.ToString(), out var transactionInfo);

View File

@ -0,0 +1,72 @@
#nullable enable
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.PayoutProcessors;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Controllers.Greenfield
{
[ApiController]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public class GreenfieldStorePayoutProcessorsController : ControllerBase
{
private readonly PayoutProcessorService _payoutProcessorService;
private readonly IEnumerable<IPayoutProcessorFactory> _factories;
public GreenfieldStorePayoutProcessorsController(PayoutProcessorService payoutProcessorService, IEnumerable<IPayoutProcessorFactory> factories)
{
_payoutProcessorService = payoutProcessorService;
_factories = factories;
}
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}/payout-processors")]
public async Task<IActionResult> GetStorePayoutProcessors(
string storeId)
{
var configured =
(await _payoutProcessorService.GetProcessors(
new PayoutProcessorService.PayoutProcessorQuery() { Stores = new[] { storeId } }))
.GroupBy(data => data.Processor).Select(datas => new PayoutProcessorData()
{
Name = datas.Key,
FriendlyName = _factories.FirstOrDefault(factory => factory.Processor == datas.Key)?.FriendlyName,
PaymentMethods = datas.Select(data => data.PaymentMethod).ToArray()
});
return Ok(configured);
}
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpDelete("~/api/v1/stores/{storeId}/payout-processors/{processor}/{paymentMethod}")]
public async Task<IActionResult> RemoveStorePayoutProcessor(
string storeId,string processor,string paymentMethod)
{
var matched =
(await _payoutProcessorService.GetProcessors(
new PayoutProcessorService.PayoutProcessorQuery()
{
Stores = new[] { storeId },
Processors = new []{ processor},
PaymentMethods = new []{paymentMethod}
})).FirstOrDefault();
if (matched is null)
{
return NotFound();
}
var tcs = new TaskCompletionSource();
_payoutProcessorService.EventAggregator.Publish(new PayoutProcessorUpdated()
{
Id = matched.Id,
Processed = tcs
});
await tcs.Task;
return Ok();
}
}
}

View File

@ -43,7 +43,7 @@ namespace BTCPayServer.Controllers.Greenfield
{
return Ok((await StoreRepository.GetWebhooks(CurrentStoreId))
.Select(o => FromModel(o, false))
.ToList());
.ToArray());
}
else
{

View File

@ -74,8 +74,8 @@ namespace BTCPayServer.Controllers.Greenfield
return UserNotFound();
}
[Authorize(Policy = Policies.CanModifyServerSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpPost("~/api/v1/users/{idOrEmail}/toggle")]
public async Task<IActionResult> ToggleUser(string idOrEmail, bool enabled )
[HttpPost("~/api/v1/users/{idOrEmail}/lock")]
public async Task<IActionResult> LockUser(string idOrEmail, LockUserRequest request )
{
var user = (await _userManager.FindByIdAsync(idOrEmail) ) ?? await _userManager.FindByEmailAsync(idOrEmail);
if (user is null)
@ -83,10 +83,10 @@ namespace BTCPayServer.Controllers.Greenfield
return UserNotFound();
}
await _userService.ToggleUser(user.Id, enabled ? null : DateTimeOffset.MaxValue);
await _userService.ToggleUser(user.Id, request.Locked ? DateTimeOffset.MaxValue : null);
return Ok();
}
[Authorize(Policy = Policies.CanViewUsers, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/users/")]
public async Task<ActionResult<ApplicationUserData[]>> GetUsers()

View File

@ -65,6 +65,10 @@ namespace BTCPayServer.Controllers.Greenfield
private readonly GreenfieldStorePaymentMethodsController _storePaymentMethodsController;
private readonly GreenfieldStoreEmailController _greenfieldStoreEmailController;
private readonly GreenfieldStoreUsersController _greenfieldStoreUsersController;
private readonly GreenfieldStorePayoutProcessorsController _greenfieldStorePayoutProcessorsController;
private readonly GreenfieldPayoutProcessorsController _greenfieldPayoutProcessorsController;
private readonly GreenfieldStoreAutomatedOnChainPayoutProcessorsController _greenfieldStoreAutomatedOnChainPayoutProcessorsController;
private readonly GreenfieldStoreAutomatedLightningPayoutProcessorsController _greenfieldStoreAutomatedLightningPayoutProcessorsController;
private readonly IServiceProvider _serviceProvider;
public BTCPayServerClientFactory(StoreRepository storeRepository,
@ -90,6 +94,10 @@ namespace BTCPayServer.Controllers.Greenfield
GreenfieldStorePaymentMethodsController storePaymentMethodsController,
GreenfieldStoreEmailController greenfieldStoreEmailController,
GreenfieldStoreUsersController greenfieldStoreUsersController,
GreenfieldStorePayoutProcessorsController greenfieldStorePayoutProcessorsController,
GreenfieldPayoutProcessorsController greenfieldPayoutProcessorsController,
GreenfieldStoreAutomatedOnChainPayoutProcessorsController greenfieldStoreAutomatedOnChainPayoutProcessorsController,
GreenfieldStoreAutomatedLightningPayoutProcessorsController greenfieldStoreAutomatedLightningPayoutProcessorsController,
IServiceProvider serviceProvider)
{
_storeRepository = storeRepository;
@ -115,6 +123,10 @@ namespace BTCPayServer.Controllers.Greenfield
_storePaymentMethodsController = storePaymentMethodsController;
_greenfieldStoreEmailController = greenfieldStoreEmailController;
_greenfieldStoreUsersController = greenfieldStoreUsersController;
_greenfieldStorePayoutProcessorsController = greenfieldStorePayoutProcessorsController;
_greenfieldPayoutProcessorsController = greenfieldPayoutProcessorsController;
_greenfieldStoreAutomatedOnChainPayoutProcessorsController = greenfieldStoreAutomatedOnChainPayoutProcessorsController;
_greenfieldStoreAutomatedLightningPayoutProcessorsController = greenfieldStoreAutomatedLightningPayoutProcessorsController;
_serviceProvider = serviceProvider;
}
@ -188,14 +200,18 @@ namespace BTCPayServer.Controllers.Greenfield
_storePaymentMethodsController,
_greenfieldStoreEmailController,
_greenfieldStoreUsersController,
new LocalHttpContextAccessor() {HttpContext = context}
_greenfieldStorePayoutProcessorsController,
_greenfieldPayoutProcessorsController,
_greenfieldStoreAutomatedOnChainPayoutProcessorsController,
_greenfieldStoreAutomatedLightningPayoutProcessorsController,
new LocalHttpContextAccessor() { HttpContext = context }
);
}
}
public class LocalHttpContextAccessor : IHttpContextAccessor
{
public HttpContext? HttpContext { get; set; }
public HttpContext HttpContext { get; set; }
}
public class LocalBTCPayServerClient : BTCPayServerClient
@ -222,6 +238,10 @@ namespace BTCPayServer.Controllers.Greenfield
private readonly UIHomeController _homeController;
private readonly GreenfieldStorePaymentMethodsController _storePaymentMethodsController;
private readonly GreenfieldStoreEmailController _greenfieldStoreEmailController;
private readonly GreenfieldStorePayoutProcessorsController _greenfieldStorePayoutProcessorsController;
private readonly GreenfieldPayoutProcessorsController _greenfieldPayoutProcessorsController;
private readonly GreenfieldStoreAutomatedOnChainPayoutProcessorsController _greenfieldStoreAutomatedOnChainPayoutProcessorsController;
private readonly GreenfieldStoreAutomatedLightningPayoutProcessorsController _greenfieldStoreAutomatedLightningPayoutProcessorsController;
private readonly GreenfieldStoreUsersController _greenfieldStoreUsersController;
public LocalBTCPayServerClient(
@ -246,6 +266,10 @@ namespace BTCPayServer.Controllers.Greenfield
GreenfieldStorePaymentMethodsController storePaymentMethodsController,
GreenfieldStoreEmailController greenfieldStoreEmailController,
GreenfieldStoreUsersController greenfieldStoreUsersController,
GreenfieldStorePayoutProcessorsController greenfieldStorePayoutProcessorsController,
GreenfieldPayoutProcessorsController greenfieldPayoutProcessorsController,
GreenfieldStoreAutomatedOnChainPayoutProcessorsController greenfieldStoreAutomatedOnChainPayoutProcessorsController,
GreenfieldStoreAutomatedLightningPayoutProcessorsController greenfieldStoreAutomatedLightningPayoutProcessorsController,
IHttpContextAccessor httpContextAccessor) : base(new Uri("https://dummy.local"), "", "")
{
_chainPaymentMethodsController = chainPaymentMethodsController;
@ -268,6 +292,10 @@ namespace BTCPayServer.Controllers.Greenfield
_storePaymentMethodsController = storePaymentMethodsController;
_greenfieldStoreEmailController = greenfieldStoreEmailController;
_greenfieldStoreUsersController = greenfieldStoreUsersController;
_greenfieldStorePayoutProcessorsController = greenfieldStorePayoutProcessorsController;
_greenfieldPayoutProcessorsController = greenfieldPayoutProcessorsController;
_greenfieldStoreAutomatedOnChainPayoutProcessorsController = greenfieldStoreAutomatedOnChainPayoutProcessorsController;
_greenfieldStoreAutomatedLightningPayoutProcessorsController = greenfieldStoreAutomatedLightningPayoutProcessorsController;
var controllers = new[]
{
@ -276,7 +304,11 @@ namespace BTCPayServer.Controllers.Greenfield
storeLightningNetworkPaymentMethodsController, greenFieldInvoiceController, storeWebhooksController,
greenFieldServerInfoController, greenfieldPullPaymentController, storesController, homeController,
lightningNodeApiController, storeLightningNodeApiController as ControllerBase,
storePaymentMethodsController, greenfieldStoreEmailController, greenfieldStoreUsersController
storePaymentMethodsController, greenfieldStoreEmailController, greenfieldStoreUsersController,
lightningNodeApiController, storeLightningNodeApiController as ControllerBase, storePaymentMethodsController,
greenfieldStoreEmailController, greenfieldStorePayoutProcessorsController, greenfieldPayoutProcessorsController,
greenfieldStoreAutomatedOnChainPayoutProcessorsController,
greenfieldStoreAutomatedLightningPayoutProcessorsController,
};
var authoverride = new DefaultAuthorizationService(
@ -447,8 +479,7 @@ namespace BTCPayServer.Controllers.Greenfield
}
public override async Task<PayoutData> ApprovePayout(string storeId, string payoutId,
ApprovePayoutRequest request,
CancellationToken cancellationToken = default)
ApprovePayoutRequest request, CancellationToken cancellationToken = default)
{
return GetFromActionResult<PayoutData>(
await _greenfieldPullPaymentController.ApprovePayout(storeId, payoutId, request, cancellationToken));
@ -458,12 +489,11 @@ namespace BTCPayServer.Controllers.Greenfield
CancellationToken token = default)
{
return GetFromActionResult<LightningNodeInformationData>(
await _storeLightningNodeApiController.GetInfo(cryptoCode));
await _storeLightningNodeApiController.GetInfo(cryptoCode, token));
}
public override async Task ConnectToLightningNode(string storeId, string cryptoCode,
ConnectToNodeRequest request,
CancellationToken token = default)
ConnectToNodeRequest request, CancellationToken token = default)
{
HandleActionResult(await _storeLightningNodeApiController.ConnectToNode(cryptoCode, request));
}
@ -472,14 +502,14 @@ namespace BTCPayServer.Controllers.Greenfield
string cryptoCode, CancellationToken token = default)
{
return GetFromActionResult<IEnumerable<LightningChannelData>>(
await _storeLightningNodeApiController.GetChannels(cryptoCode));
await _storeLightningNodeApiController.GetChannels(cryptoCode, token));
}
public override async Task OpenLightningChannel(string storeId, string cryptoCode,
OpenLightningChannelRequest request,
CancellationToken token = default)
{
HandleActionResult(await _storeLightningNodeApiController.OpenChannel(cryptoCode, request));
HandleActionResult(await _storeLightningNodeApiController.OpenChannel(cryptoCode, request, token));
}
public override async Task<string> GetLightningDepositAddress(string storeId, string cryptoCode,
@ -490,25 +520,23 @@ namespace BTCPayServer.Controllers.Greenfield
}
public override async Task PayLightningInvoice(string storeId, string cryptoCode,
PayLightningInvoiceRequest request,
CancellationToken token = default)
PayLightningInvoiceRequest request, CancellationToken token = default)
{
HandleActionResult(await _storeLightningNodeApiController.PayInvoice(cryptoCode, request));
HandleActionResult(await _storeLightningNodeApiController.PayInvoice(cryptoCode, request, token));
}
public override async Task<LightningInvoiceData> GetLightningInvoice(string storeId, string cryptoCode,
string invoiceId, CancellationToken token = default)
{
return GetFromActionResult<LightningInvoiceData>(
await _storeLightningNodeApiController.GetInvoice(cryptoCode, invoiceId));
await _storeLightningNodeApiController.GetInvoice(cryptoCode, invoiceId, token));
}
public override async Task<LightningInvoiceData> CreateLightningInvoice(string storeId, string cryptoCode,
CreateLightningInvoiceRequest request,
CancellationToken token = default)
CreateLightningInvoiceRequest request, CancellationToken token = default)
{
return GetFromActionResult<LightningInvoiceData>(
await _storeLightningNodeApiController.CreateInvoice(cryptoCode, request));
await _storeLightningNodeApiController.CreateInvoice(cryptoCode, request, token));
}
public override async Task<LightningNodeInformationData> GetLightningNodeInfo(string cryptoCode,
@ -528,13 +556,13 @@ namespace BTCPayServer.Controllers.Greenfield
CancellationToken token = default)
{
return GetFromActionResult<IEnumerable<LightningChannelData>>(
await _lightningNodeApiController.GetChannels(cryptoCode));
await _lightningNodeApiController.GetChannels(cryptoCode, token));
}
public override async Task OpenLightningChannel(string cryptoCode, OpenLightningChannelRequest request,
CancellationToken token = default)
{
HandleActionResult(await _lightningNodeApiController.OpenChannel(cryptoCode, request));
HandleActionResult(await _lightningNodeApiController.OpenChannel(cryptoCode, request, token));
}
public override async Task<string> GetLightningDepositAddress(string cryptoCode,
@ -545,18 +573,17 @@ namespace BTCPayServer.Controllers.Greenfield
}
public override async Task<LightningPaymentData> PayLightningInvoice(string cryptoCode,
PayLightningInvoiceRequest request,
CancellationToken token = default)
PayLightningInvoiceRequest request, CancellationToken token = default)
{
return GetFromActionResult<LightningPaymentData>(
await _lightningNodeApiController.PayInvoice(cryptoCode, request));
await _lightningNodeApiController.PayInvoice(cryptoCode, request, token));
}
public override async Task<LightningInvoiceData> GetLightningInvoice(string cryptoCode, string invoiceId,
CancellationToken token = default)
{
return GetFromActionResult<LightningInvoiceData>(
await _lightningNodeApiController.GetInvoice(cryptoCode, invoiceId));
await _lightningNodeApiController.GetInvoice(cryptoCode, invoiceId, token));
}
public override async Task<LightningInvoiceData> CreateLightningInvoice(string cryptoCode,
@ -564,21 +591,18 @@ namespace BTCPayServer.Controllers.Greenfield
CancellationToken token = default)
{
return GetFromActionResult<LightningInvoiceData>(
await _lightningNodeApiController.CreateInvoice(cryptoCode, request));
await _lightningNodeApiController.CreateInvoice(cryptoCode, request, token));
}
private T GetFromActionResult<T>(IActionResult result)
{
HandleActionResult(result);
switch (result)
return result switch
{
case JsonResult jsonResult:
return (T)jsonResult.Value;
case OkObjectResult {Value: T res}:
return res;
default:
return default;
}
JsonResult jsonResult => (T)jsonResult.Value,
OkObjectResult { Value: T res } => res,
_ => default
};
}
private void HandleActionResult(IActionResult result)
@ -607,7 +631,7 @@ namespace BTCPayServer.Controllers.Greenfield
}
public override Task<IEnumerable<OnChainPaymentMethodData>> GetStoreOnChainPaymentMethods(string storeId,
bool? enabled, CancellationToken token)
bool? enabled, CancellationToken token = default)
{
return Task.FromResult(
GetFromActionResult(_chainPaymentMethodsController.GetOnChainPaymentMethods(storeId, enabled)));
@ -627,8 +651,7 @@ namespace BTCPayServer.Controllers.Greenfield
}
public override async Task<OnChainPaymentMethodData> UpdateStoreOnChainPaymentMethod(string storeId,
string cryptoCode, UpdateOnChainPaymentMethodRequest paymentMethod,
CancellationToken token = default)
string cryptoCode, UpdateOnChainPaymentMethodRequest paymentMethod, CancellationToken token = default)
{
return GetFromActionResult<OnChainPaymentMethodData>(
await _chainPaymentMethodsController.UpdateOnChainPaymentMethod(storeId, cryptoCode,
@ -651,8 +674,7 @@ namespace BTCPayServer.Controllers.Greenfield
}
public override Task<OnChainPaymentMethodPreviewResultData> PreviewStoreOnChainPaymentMethodAddresses(
string storeId, string cryptoCode, int offset = 0, int amount = 10,
CancellationToken token = default)
string storeId, string cryptoCode, int offset = 0, int amount = 10, CancellationToken token = default)
{
return Task.FromResult(GetFromActionResult<OnChainPaymentMethodPreviewResultData>(
_chainPaymentMethodsController.GetOnChainPaymentMethodPreview(storeId, cryptoCode, offset,
@ -781,7 +803,7 @@ namespace BTCPayServer.Controllers.Greenfield
}
public override async Task<IEnumerable<OnChainWalletTransactionData>> ShowOnChainWalletTransactions(
string storeId, string cryptoCode, TransactionStatus[] statusFilter = null,
string storeId, string cryptoCode, TransactionStatus[] statusFilter = null, string labelFilter = null,
CancellationToken token = default)
{
return GetFromActionResult<IEnumerable<OnChainWalletTransactionData>>(
@ -789,8 +811,7 @@ namespace BTCPayServer.Controllers.Greenfield
}
public override async Task<OnChainWalletTransactionData> GetOnChainWalletTransaction(string storeId,
string cryptoCode, string transactionId,
CancellationToken token = default)
string cryptoCode, string transactionId, CancellationToken token = default)
{
return GetFromActionResult<OnChainWalletTransactionData>(
await _storeOnChainWalletsController.GetOnChainWalletTransaction(storeId, cryptoCode, transactionId));
@ -804,8 +825,7 @@ namespace BTCPayServer.Controllers.Greenfield
}
public override async Task<OnChainWalletTransactionData> CreateOnChainTransaction(string storeId,
string cryptoCode, CreateOnChainTransactionRequest request,
CancellationToken token = default)
string cryptoCode, CreateOnChainTransactionRequest request, CancellationToken token = default)
{
if (!request.ProceedWithBroadcast)
{
@ -1100,10 +1120,12 @@ namespace BTCPayServer.Controllers.Greenfield
{
return GetFromActionResult<ApplicationUserData>(await _usersController.GetUser(idOrEmail));
}
public override async Task ToggleUser(string idOrEmail, bool enabled, CancellationToken token = default)
public override async Task LockUser(string idOrEmail, bool disabled, CancellationToken token = default)
{
HandleActionResult(await _usersController.ToggleUser(idOrEmail, enabled));
HandleActionResult(await _usersController.LockUser(idOrEmail, new LockUserRequest()
{
Locked = disabled
}));
}
public override async Task<OnChainWalletTransactionData> PatchOnChainWalletTransaction(string storeId, string cryptoCode, string transactionId,
@ -1114,12 +1136,73 @@ namespace BTCPayServer.Controllers.Greenfield
public override async Task<LightningPaymentData> GetLightningPayment(string cryptoCode, string paymentHash, CancellationToken token = default)
{
return GetFromActionResult<LightningPaymentData>(await _lightningNodeApiController.GetPayment(cryptoCode, paymentHash));
return GetFromActionResult<LightningPaymentData>(await _lightningNodeApiController.GetPayment(cryptoCode, paymentHash, token));
}
public override async Task<LightningPaymentData> GetLightningPayment(string storeId, string cryptoCode, string paymentHash, CancellationToken token = default)
{
return GetFromActionResult<LightningPaymentData>(await _storeLightningNodeApiController.GetPayment(cryptoCode, paymentHash));
return GetFromActionResult<LightningPaymentData>(await _storeLightningNodeApiController.GetPayment(cryptoCode, paymentHash, token));
}
public override async Task<PayoutData> CreatePayout(string storeId, CreatePayoutThroughStoreRequest payoutRequest,
CancellationToken cancellationToken = default)
{
return GetFromActionResult<PayoutData>(
await _greenfieldPullPaymentController.CreatePayoutThroughStore(storeId, payoutRequest));
}
public override async Task<IEnumerable<PayoutProcessorData>> GetPayoutProcessors(string storeId, CancellationToken token = default)
{
return GetFromActionResult<IEnumerable<PayoutProcessorData>>(await _greenfieldStorePayoutProcessorsController.GetStorePayoutProcessors(storeId));
}
public override Task<IEnumerable<PayoutProcessorData>> GetPayoutProcessors(CancellationToken token = default)
{
return Task.FromResult(GetFromActionResult<IEnumerable<PayoutProcessorData>>(_greenfieldPayoutProcessorsController.GetPayoutProcessors()));
}
public override async Task RemovePayoutProcessor(string storeId, string processor, string paymentMethod, CancellationToken token = default)
{
HandleActionResult(await _greenfieldStorePayoutProcessorsController.RemoveStorePayoutProcessor(storeId, processor, paymentMethod));
}
public override async Task<IEnumerable<OnChainAutomatedPayoutSettings>>
GetStoreOnChainAutomatedPayoutProcessors(string storeId, string paymentMethod = null,
CancellationToken token = default)
{
return GetFromActionResult<IEnumerable<OnChainAutomatedPayoutSettings>>(
await _greenfieldStoreAutomatedOnChainPayoutProcessorsController
.GetStoreOnChainAutomatedPayoutProcessors(storeId, paymentMethod));
}
public override async Task<IEnumerable<LightningAutomatedPayoutSettings>> GetStoreLightningAutomatedPayoutProcessors(string storeId, string paymentMethod = null,
CancellationToken token = default)
{
return GetFromActionResult<IEnumerable<LightningAutomatedPayoutSettings>>(
await _greenfieldStoreAutomatedLightningPayoutProcessorsController
.GetStoreLightningAutomatedPayoutProcessors(storeId, paymentMethod));
}
public override async Task<OnChainAutomatedPayoutSettings> UpdateStoreOnChainAutomatedPayoutProcessors(string storeId, string paymentMethod,
OnChainAutomatedPayoutSettings request, CancellationToken token = default)
{
return GetFromActionResult<OnChainAutomatedPayoutSettings>(
await _greenfieldStoreAutomatedOnChainPayoutProcessorsController
.UpdateStoreOnchainAutomatedPayoutProcessor(storeId, paymentMethod, request));
}
public override async Task<LightningAutomatedPayoutSettings> UpdateStoreLightningAutomatedPayoutProcessors(string storeId, string paymentMethod,
LightningAutomatedPayoutSettings request, CancellationToken token = default)
{
return GetFromActionResult<LightningAutomatedPayoutSettings>(
await _greenfieldStoreAutomatedLightningPayoutProcessorsController
.UpdateStoreLightningAutomatedPayoutProcessor(storeId, paymentMethod, request));
}
public override async Task<PayoutData[]> GetStorePayouts(string storeId, bool includeCancelled = false, CancellationToken cancellationToken = default)
{
return GetFromActionResult<PayoutData[]>(
await _greenfieldPullPaymentController
.GetStorePayouts(storeId,includeCancelled));
}
}
}

View File

@ -0,0 +1,122 @@
#nullable enable
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
namespace BTCPayServer;
public class LightningAddressService
{
private readonly ApplicationDbContextFactory _applicationDbContextFactory;
private readonly IMemoryCache _memoryCache;
public LightningAddressService(ApplicationDbContextFactory applicationDbContextFactory, IMemoryCache memoryCache)
{
_applicationDbContextFactory = applicationDbContextFactory;
_memoryCache = memoryCache;
}
public async Task<List<LightningAddressData>> Get(LightningAddressQuery query)
{
await using var context = _applicationDbContextFactory.CreateContext();
return await GetCore(context, query);
}
private async Task<List<LightningAddressData>> GetCore(ApplicationDbContext context, LightningAddressQuery query)
{
IQueryable<LightningAddressData?> queryable = context.LightningAddresses.AsQueryable();
query.Usernames = query.Usernames?.Select(NormalizeUsername)?.ToArray();
if (query.Usernames is not null)
{
queryable = queryable.Where(data => query.Usernames.Contains(data!.Username));
}
if (query.StoreIds is not null)
{
queryable = queryable.Where(data => query.StoreIds.Contains(data!.StoreDataId));
}
#pragma warning disable CS8619 // Nullability of reference types in value doesn't match target type.
return await queryable.ToListAsync();
#pragma warning restore CS8619 // Nullability of reference types in value doesn't match target type.
}
public async Task<LightningAddressData?> ResolveByAddress(string username)
{
return await _memoryCache.GetOrCreateAsync(GetKey(username), async entry =>
{
var result = await Get(new LightningAddressQuery() {Usernames = new[] {username}});
return result.FirstOrDefault();
});
}
private string NormalizeUsername(string username)
{
return username.ToLowerInvariant();
}
public async Task<bool> Set(LightningAddressData data)
{
await using var context = _applicationDbContextFactory.CreateContext();
var result = (await GetCore(context, new LightningAddressQuery() {Usernames = new[] {data.Username}}))
.FirstOrDefault();
if (result is not null)
{
if (result.StoreDataId != data.StoreDataId)
{
return false;
}
context.Remove(result);
}
data.Username = NormalizeUsername(data.Username);
await context.AddAsync(data);
await context.SaveChangesAsync();
_memoryCache.Remove(GetKey(data.Username));
return true;
}
public async Task<bool> Remove(string username, string? storeId = null)
{
await using var context = _applicationDbContextFactory.CreateContext();
var x = (await GetCore(context, new LightningAddressQuery() {Usernames = new[] {username}})).FirstOrDefault();
if (x is null) return true;
if (storeId is not null && x.StoreDataId != storeId)
{
return false;
}
context.Remove(x);
await context.SaveChangesAsync();
_memoryCache.Remove(GetKey(username));
return true;
}
public async Task Set(LightningAddressData data, ApplicationDbContext context)
{
var result = (await GetCore(context, new LightningAddressQuery() {Usernames = new[] {data.Username}}))
.FirstOrDefault();
if (result is not null)
{
if (result.StoreDataId != data.StoreDataId)
{
return;
}
context.Remove(result);
}
await context.AddAsync(data);
}
private string GetKey(string username)
{
username = NormalizeUsername(username);
return $"{nameof(LightningAddressService)}_{username}";
}
}

View File

@ -144,4 +144,11 @@ namespace BTCPayServer
return await context.Fido2Credentials.Where(fDevice => fDevice.ApplicationUserId == userId && fDevice.Type == Fido2Credential.CredentialType.LNURLAuth).AnyAsync();
}
}
public class LightningAddressQuery
{
public string[] StoreIds { get; set; }
public string[] Usernames { get; set; }
}
}

View File

@ -209,7 +209,7 @@ namespace BTCPayServer.Controllers
if (result.IsLockedOut)
{
_logger.LogWarning($"User '{user.Id}' account locked out.");
return RedirectToAction(nameof(Lockout));
return RedirectToAction(nameof(Lockout), new { user.LockoutEnd});
}
else
{
@ -428,7 +428,7 @@ namespace BTCPayServer.Controllers
else if (result.IsLockedOut)
{
_logger.LogWarning("User with ID {UserId} account locked out.", user.Id);
return RedirectToAction(nameof(Lockout));
return RedirectToAction(nameof(Lockout), new { user.LockoutEnd});
}
else
{
@ -497,7 +497,8 @@ namespace BTCPayServer.Controllers
if (result.IsLockedOut)
{
_logger.LogWarning("User with ID {UserId} account locked out.", user.Id);
return RedirectToAction(nameof(Lockout));
return RedirectToAction(nameof(Lockout), new { user.LockoutEnd});
}
else
{
@ -509,9 +510,9 @@ namespace BTCPayServer.Controllers
[HttpGet("/login/lockout")]
[AllowAnonymous]
public IActionResult Lockout()
public IActionResult Lockout(DateTimeOffset? lockoutEnd)
{
return View();
return View(lockoutEnd);
}
[HttpGet("/register")]

View File

@ -99,27 +99,29 @@ namespace BTCPayServer.Controllers
ModelState.AddModelError(nameof(vm.ResetEveryAmount), "You must reset the goal at a minimum of 1 ");
}
if (vm.DisplayPerksRanking && !vm.SortPerksByPopularity)
if (vm.DisplayPerksRanking)
{
ModelState.AddModelError(nameof(vm.DisplayPerksRanking), "You must sort by popularity in order to display ranking.");
vm.SortPerksByPopularity = true;
}
var parsedSounds = vm.Sounds.Split(
var parsedSounds = vm.Sounds?.Split(
new[] { "\r\n", "\r", "\n" },
StringSplitOptions.None
).Select(s => s.Trim()).ToArray();
if (vm.SoundsEnabled && !parsedSounds.Any())
if (vm.SoundsEnabled && (parsedSounds == null || !parsedSounds.Any()))
{
ModelState.AddModelError(nameof(vm.Sounds), "You must have at least one sound if you enable sounds");
vm.SoundsEnabled = false;
parsedSounds = new CrowdfundSettings().Sounds;
}
var parsedAnimationColors = vm.AnimationColors.Split(
var parsedAnimationColors = vm.AnimationColors?.Split(
new[] { "\r\n", "\r", "\n" },
StringSplitOptions.None
).Select(s => s.Trim()).ToArray();
if (vm.AnimationsEnabled && !parsedAnimationColors.Any())
if (vm.AnimationsEnabled && (parsedAnimationColors == null || !parsedAnimationColors.Any()))
{
ModelState.AddModelError(nameof(vm.AnimationColors), "You must have at least one animation color if you enable animations");
vm.AnimationsEnabled = false;
parsedAnimationColors = new CrowdfundSettings().AnimationColors;
}
if (!ModelState.IsValid)

View File

@ -43,8 +43,10 @@ namespace BTCPayServer.Controllers
[HttpGet("/apps/{appId}")]
public async Task<IActionResult> RedirectToApp(string appId)
{
switch ((await _AppService.GetApp(appId, null)).AppType)
var app = await _AppService.GetApp(appId, null);
if (app is null)
return NotFound();
switch (app.AppType)
{
case nameof(AppType.Crowdfund):
return RedirectToAction("ViewCrowdfund", new { appId });
@ -219,7 +221,7 @@ namespace BTCPayServer.Controllers
Currency = settings.Currency,
Price = price,
BuyerEmail = email,
OrderId = orderId,
OrderId = orderId ?? AppService.GetPosOrderId(appId),
NotificationURL =
string.IsNullOrEmpty(notificationUrl) ? settings.NotificationUrl : notificationUrl,
RedirectURL = !string.IsNullOrEmpty(redirectUrl) ? redirectUrl

View File

@ -135,6 +135,12 @@ namespace BTCPayServer.Controllers
var servers = new JArray();
servers.Add(new JObject(new JProperty("url", HttpContext.Request.GetAbsoluteRoot())));
json["servers"] = servers;
var tags = (JArray)json["tags"];
json["tags"] = new JArray(tags
.Select(o => (name: ((JObject)o)["name"].Value<string>(), o))
.OrderBy(o => o.name)
.Select(o => o.o)
.ToArray());
return Json(json);
}
[Route("docs")]

View File

@ -224,7 +224,7 @@ namespace BTCPayServer.Controllers
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public async Task<IActionResult> Refund(string invoiceId, RefundModel model, CancellationToken cancellationToken)
{
using var ctx = _dbContextFactory.CreateContext();
await using var ctx = _dbContextFactory.CreateContext();
var invoice = GetCurrentInvoice();
if (invoice == null)
@ -288,21 +288,25 @@ namespace BTCPayServer.Controllers
Name = $"Refund {invoice.Id}",
PaymentMethodIds = new[] { paymentMethodId },
StoreId = invoice.StoreId,
BOLT11Expiration = store.GetStoreBlob().RefundBOLT11Expiration
BOLT11Expiration = store.GetStoreBlob().RefundBOLT11Expiration,
//AutoApproveClaims = true
};
switch (model.SelectedRefundOption)
{
case "RateThen":
createPullPayment.Currency = paymentMethodId.CryptoCode;
createPullPayment.Amount = model.CryptoAmountThen;
createPullPayment.AutoApproveClaims = true;
break;
case "CurrentRate":
createPullPayment.Currency = paymentMethodId.CryptoCode;
createPullPayment.Amount = model.CryptoAmountNow;
createPullPayment.AutoApproveClaims = true;
break;
case "Fiat":
createPullPayment.Currency = invoice.Currency;
createPullPayment.Amount = model.FiatAmount;
createPullPayment.AutoApproveClaims = false;
break;
case "Custom":
model.Title = "How much to refund?";
@ -348,10 +352,11 @@ namespace BTCPayServer.Controllers
createPullPayment = new CreatePullPayment
{
Name = $"Refund {invoice.Id}",
PaymentMethodIds = new[] { paymentMethodId },
PaymentMethodIds = new[] {paymentMethodId},
StoreId = invoice.StoreId,
Currency = model.CustomCurrency,
Amount = model.CustomAmount
Amount = model.CustomAmount,
AutoApproveClaims = paymentMethodId.CryptoCode == model.CustomCurrency
};
break;
default:
@ -568,8 +573,8 @@ namespace BTCPayServer.Controllers
}
if (paymentMethodId is null)
{
paymentMethodId = enabledPaymentIds.FirstOrDefault(e => e.CryptoCode == "BTC" && e.PaymentType == PaymentTypes.BTCLike) ??
enabledPaymentIds.FirstOrDefault(e => e.CryptoCode == "BTC" && e.PaymentType == PaymentTypes.LightningLike) ??
paymentMethodId = enabledPaymentIds.FirstOrDefault(e => e.CryptoCode == _NetworkProvider.DefaultNetwork.CryptoCode && e.PaymentType == PaymentTypes.BTCLike) ??
enabledPaymentIds.FirstOrDefault(e => e.CryptoCode == _NetworkProvider.DefaultNetwork.CryptoCode && e.PaymentType == PaymentTypes.LightningLike) ??
enabledPaymentIds.FirstOrDefault();
}
isDefaultPaymentId = true;
@ -689,7 +694,7 @@ namespace BTCPayServer.Controllers
})
};
}).Where(c => c.CryptoImage != "/")
.OrderByDescending(a => a.CryptoCode == "BTC").ThenBy(a => a.PaymentMethodName).ThenBy(a => a.IsLightning ? 1 : 0)
.OrderByDescending(a => a.CryptoCode == _NetworkProvider.DefaultNetwork.CryptoCode).ThenBy(a => a.PaymentMethodName).ThenBy(a => a.IsLightning ? 1 : 0)
.ToList()
};
paymentMethodHandler.PreparePaymentModel(model, dto, storeBlob, paymentMethod);
@ -816,7 +821,6 @@ namespace BTCPayServer.Controllers
InvoiceQuery invoiceQuery = GetInvoiceQuery(model.SearchTerm, model.TimezoneOffset ?? 0);
invoiceQuery.StoreId = model.StoreIds;
var counting = _InvoiceRepository.GetInvoicesTotal(invoiceQuery);
invoiceQuery.Take = model.Count;
invoiceQuery.Skip = model.Skip;
var list = await _InvoiceRepository.GetInvoices(invoiceQuery);
@ -840,7 +844,6 @@ namespace BTCPayServer.Controllers
Details = InvoicePopulatePayments(invoice),
});
}
model.Total = await counting;
return View(model);
}

View File

@ -2,7 +2,6 @@ using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
@ -18,7 +17,6 @@ using BTCPayServer.Lightning;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Services;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
@ -26,7 +24,6 @@ using BTCPayServer.Services.Stores;
using LNURL;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Routing;
using NBitcoin;
using NBitcoin.Crypto;
@ -44,9 +41,10 @@ namespace BTCPayServer
private readonly LightningLikePaymentHandler _lightningLikePaymentHandler;
private readonly StoreRepository _storeRepository;
private readonly AppService _appService;
private readonly UIInvoiceController _invoiceController;
private readonly SettingsRepository _settingsRepository;
private readonly LinkGenerator _linkGenerator;
private readonly LightningAddressService _lightningAddressService;
public UILNURLController(InvoiceRepository invoiceRepository,
EventAggregator eventAggregator,
@ -55,8 +53,8 @@ namespace BTCPayServer
StoreRepository storeRepository,
AppService appService,
UIInvoiceController invoiceController,
SettingsRepository settingsRepository,
LinkGenerator linkGenerator)
LinkGenerator linkGenerator,
LightningAddressService lightningAddressService)
{
_invoiceRepository = invoiceRepository;
_eventAggregator = eventAggregator;
@ -65,8 +63,8 @@ namespace BTCPayServer
_storeRepository = storeRepository;
_appService = appService;
_invoiceController = invoiceController;
_settingsRepository = settingsRepository;
_linkGenerator = linkGenerator;
_lightningAddressService = lightningAddressService;
}
@ -90,6 +88,7 @@ namespace BTCPayServer
{
return NotFound();
}
if (string.IsNullOrEmpty(itemCode))
{
return NotFound();
@ -133,7 +132,7 @@ namespace BTCPayServer
}
return await GetLNURL(cryptoCode, app.StoreDataId, currencyCode, null, null,
() => (null, new List<string> { AppService.GetAppInternalTag(appId) }, item.Price.Value, true));
() => (null, app, item, new List<string> {AppService.GetAppInternalTag(appId)}, item.Price.Value, true));
}
public class EditLightningAddressVM
@ -146,7 +145,7 @@ namespace BTCPayServer
}
public EditLightningAddressItem Add { get; set; }
public List<EditLightningAddressItem> Items { get; set; } = new List<EditLightningAddressItem>();
public List<EditLightningAddressItem> Items { get; set; } = new();
}
public class LightningAddressSettings
@ -154,8 +153,7 @@ namespace BTCPayServer
public class LightningAddressItem
{
public string StoreId { get; set; }
[Display(Name = "Invoice currency")]
public string CurrencyCode { get; set; }
[Display(Name = "Invoice currency")] public string CurrencyCode { get; set; }
[Display(Name = "Min sats")]
[Range(1, double.PositiveInfinity)]
@ -178,29 +176,24 @@ namespace BTCPayServer
}
}
private async Task<LightningAddressSettings> GetSettings()
{
return await _settingsRepository.GetSettingAsync<LightningAddressSettings>(nameof(LightningAddressSettings)) ??
new LightningAddressSettings();
}
[HttpGet("~/.well-known/lnurlp/{username}")]
public async Task<IActionResult> ResolveLightningAddress(string username)
{
var lightningAddressSettings = await GetSettings();
if (!lightningAddressSettings.Items.TryGetValue(username.ToLowerInvariant(), out var item))
var lightningAddressSettings = await _lightningAddressService.ResolveByAddress(username);
if (lightningAddressSettings is null)
{
return NotFound("Unknown username");
}
return await GetLNURL("BTC", item.StoreId, item.CurrencyCode, item.Min, item.Max,
() => (username, null, null, true));
var blob = lightningAddressSettings.Blob.GetBlob<LightningAddressDataBlob>();
return await GetLNURL("BTC", lightningAddressSettings.StoreDataId, blob.CurrencyCode, blob.Min, blob.Max,
() => (username, null, null, null, null, true));
}
[HttpGet("pay")]
public async Task<IActionResult> GetLNURL(string cryptoCode, string storeId, string currencyCode = null,
decimal? min = null, decimal? max = null,
Func<(string username, List<string> additionalTags, decimal? invoiceAmount, bool? anyoneCanInvoice)>
Func<(string username, AppData app, ViewPointOfSaleViewModel.Item item, List<string> additionalTags, decimal? invoiceAmount, bool? anyoneCanInvoice)>
internalDetails = null)
{
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
@ -233,8 +226,8 @@ namespace BTCPayServer
return NotFound("LNURL or Lightning payment method disabled");
}
(string username, List<string> additionalTags, decimal? invoiceAmount, bool? anyoneCanInvoice) =
(internalDetails ?? (() => (null, null, null, null)))();
(string username, AppData app, ViewPointOfSaleViewModel.Item item, List<string> additionalTags, decimal? invoiceAmount, bool? anyoneCanInvoice) =
(internalDetails ?? (() => (null, null, null, null, null, null)))();
if ((anyoneCanInvoice ?? blob.AnyoneCanInvoice) is false)
{
@ -242,22 +235,34 @@ namespace BTCPayServer
}
var lnAddress = username is null ? null : $"{username}@{Request.Host}";
List<string[]> lnurlMetadata = new List<string[]>();
List<string[]> lnurlMetadata = new();
var i = await _invoiceController.CreateInvoiceCoreRaw(
new CreateInvoiceRequest
var invoiceRequest = new CreateInvoiceRequest
{
Amount = invoiceAmount,
Checkout = new InvoiceDataBase.CheckoutOptions
{
Amount = invoiceAmount,
Checkout = new InvoiceDataBase.CheckoutOptions
PaymentMethods = new[] { pmi.ToStringNormalized() },
Expiration = blob.InvoiceExpiration < TimeSpan.FromMinutes(2)
? blob.InvoiceExpiration
: TimeSpan.FromMinutes(2)
},
Currency = currencyCode,
Type = invoiceAmount is null ? InvoiceType.TopUp : InvoiceType.Standard,
};
if (item != null)
{
invoiceRequest.Metadata =
new InvoiceMetadata
{
PaymentMethods = new[] { pmi.ToStringNormalized() },
Expiration = blob.InvoiceExpiration < TimeSpan.FromMinutes(2)
? blob.InvoiceExpiration
: TimeSpan.FromMinutes(2)
},
Currency = currencyCode,
Type = invoiceAmount is null ? InvoiceType.TopUp : InvoiceType.Standard,
}, store, Request.GetAbsoluteRoot(), additionalTags);
ItemCode = item.Id,
ItemDesc = item.Description,
OrderId = AppService.GetPosOrderId(app.Id)
}.ToJObject();
}
var i = await _invoiceController.CreateInvoiceCoreRaw(invoiceRequest, store, Request.GetAbsoluteRoot(), additionalTags);
if (i.Type != InvoiceType.TopUp)
{
min = i.GetPaymentMethod(pmi).Calculate().Due.ToDecimal(MoneyUnit.Satoshi);
@ -272,11 +277,16 @@ namespace BTCPayServer
pm.SetPaymentMethodDetails(paymentMethodDetails);
await _invoiceRepository.UpdateInvoicePaymentMethod(i.Id, pm);
}
var description = blob.LightningDescriptionTemplate
.Replace("{StoreName}", store.StoreName ?? "", StringComparison.OrdinalIgnoreCase)
.Replace("{ItemDescription}", i.Metadata.ItemDesc ?? "", StringComparison.OrdinalIgnoreCase)
.Replace("{OrderId}", i.Metadata.OrderId ?? "", StringComparison.OrdinalIgnoreCase);
lnurlMetadata.Add(new[] { "text/plain", i.Id });
lnurlMetadata.Add(new[] {"text/plain", description});
if (!string.IsNullOrEmpty(username))
{
lnurlMetadata.Add(new[] { "text/identifier", lnAddress });
lnurlMetadata.Add(new[] {"text/identifier", lnAddress});
}
return Ok(new LNURLPayRequest
@ -292,11 +302,10 @@ namespace BTCPayServer
Callback = new Uri(_linkGenerator.GetUriByAction(
action: nameof(GetLNURLForInvoice),
controller: "UILNURL",
values: new { cryptoCode, invoiceId = i.Id }, Request.Scheme, Request.Host, Request.PathBase))
values: new {cryptoCode, invoiceId = i.Id}, Request.Scheme, Request.Host, Request.PathBase))
});
}
[HttpGet("pay/i/{invoiceId}")]
public async Task<IActionResult> GetLNURLForInvoice(string invoiceId, string cryptoCode,
[FromQuery] long? amount = null, string comment = null)
@ -306,11 +315,19 @@ namespace BTCPayServer
{
return NotFound();
}
if (comment is not null)
comment = comment.Truncate(2000);
var pmi = new PaymentMethodId(cryptoCode, PaymentTypes.LNURLPay);
var i = await _invoiceRepository.GetInvoice(invoiceId, true);
var store = await _storeRepository.FindStore(i.StoreId);
if (store is null)
{
return NotFound();
}
if (i.Status == InvoiceStatusLegacy.New)
{
var isTopup = i.IsUnsetTopUp();
@ -335,22 +352,24 @@ namespace BTCPayServer
LightMoneyUnit.Satoshi);
var max = isTopup ? LightMoney.FromUnit(6.12m, LightMoneyUnit.BTC) : min;
List<string[]> lnurlMetadata = new List<string[]>();
List<string[]> lnurlMetadata = new();
lnurlMetadata.Add(new[] { "text/plain", i.Id });
var blob = store.GetStoreBlob();
var description = blob.LightningDescriptionTemplate
.Replace("{StoreName}", store.StoreName ?? "", StringComparison.OrdinalIgnoreCase)
.Replace("{ItemDescription}", i.Metadata.ItemDesc ?? "", StringComparison.OrdinalIgnoreCase)
.Replace("{OrderId}", i.Metadata.OrderId ?? "", StringComparison.OrdinalIgnoreCase);
lnurlMetadata.Add(new[] {"text/plain", description});
if (!string.IsNullOrEmpty(paymentMethodDetails.ConsumedLightningAddress))
{
lnurlMetadata.Add(new[] { "text/identifier", paymentMethodDetails.ConsumedLightningAddress });
lnurlMetadata.Add(new[] {"text/identifier", paymentMethodDetails.ConsumedLightningAddress});
}
var metadata = JsonConvert.SerializeObject(lnurlMetadata);
if (amount.HasValue && (amount < min || amount > max))
{
return BadRequest(new LNUrlStatusResponse
{
Status = "ERROR",
Reason = "Amount is out of bounds."
});
return BadRequest(new LNUrlStatusResponse {Status = "ERROR", Reason = "Amount is out of bounds."});
}
if (amount.HasValue && string.IsNullOrEmpty(paymentMethodDetails.BOLT11) ||
@ -379,7 +398,7 @@ namespace BTCPayServer
descriptionHash,
i.ExpirationTime.ToUniversalTime() - DateTimeOffset.UtcNow));
if (!BOLT11PaymentRequest.Parse(invoice.BOLT11, network.NBitcoinNetwork)
.VerifyDescriptionHash(metadata))
.VerifyDescriptionHash(metadata))
{
return BadRequest(new LNUrlStatusResponse
{
@ -413,9 +432,7 @@ namespace BTCPayServer
paymentMethodDetails, pmi));
return Ok(new LNURLPayRequest.LNURLPayRequestCallbackResponse
{
Disposable = true,
Routes = Array.Empty<string>(),
Pr = paymentMethodDetails.BOLT11
Disposable = true, Routes = Array.Empty<string>(), Pr = paymentMethodDetails.BOLT11
});
}
@ -430,9 +447,7 @@ namespace BTCPayServer
return Ok(new LNURLPayRequest.LNURLPayRequestCallbackResponse
{
Disposable = true,
Routes = Array.Empty<string>(),
Pr = paymentMethodDetails.BOLT11
Disposable = true, Routes = Array.Empty<string>(), Pr = paymentMethodDetails.BOLT11
});
}
@ -452,8 +467,7 @@ namespace BTCPayServer
return BadRequest(new LNUrlStatusResponse
{
Status = "ERROR",
Reason = "Invoice not in a valid payable state"
Status = "ERROR", Reason = "Invoice not in a valid payable state"
});
}
@ -463,37 +477,39 @@ namespace BTCPayServer
[HttpGet("~/stores/{storeId}/integrations/lightning-address")]
public async Task<IActionResult> EditLightningAddress(string storeId)
{
if (ControllerContext.HttpContext.GetStoreData().GetEnabledPaymentIds(_btcPayNetworkProvider).All(id => id.PaymentType != LNURLPayPaymentType.Instance))
if (ControllerContext.HttpContext.GetStoreData().GetEnabledPaymentIds(_btcPayNetworkProvider)
.All(id => id.PaymentType != LNURLPayPaymentType.Instance))
{
TempData.SetStatusMessageModel(new StatusMessageModel
{
Message = "LNURL is required for lightning addresses but has not yet been enabled.",
Severity = StatusMessageModel.StatusSeverity.Error
});
return RedirectToAction(nameof(UIStoresController.GeneralSettings), "UIStores", new { storeId });
}
var lightningAddressSettings = await GetSettings();
if (lightningAddressSettings.StoreToItemMap.TryGetValue(storeId, out var addresses))
{
return View(new EditLightningAddressVM
{
Items = addresses.Select(s => new EditLightningAddressVM.EditLightningAddressItem
{
Max = lightningAddressSettings.Items[s].Max,
Min = lightningAddressSettings.Items[s].Min,
CurrencyCode = lightningAddressSettings.Items[s].CurrencyCode,
StoreId = lightningAddressSettings.Items[s].StoreId,
Username = s,
}).ToList()
});
return RedirectToAction(nameof(UIStoresController.GeneralSettings), "UIStores", new {storeId});
}
var addresses =
await _lightningAddressService.Get(new LightningAddressQuery() {StoreIds = new[] {storeId}});
return View(new EditLightningAddressVM
{
Items = new List<EditLightningAddressVM.EditLightningAddressItem>()
Items = addresses.Select(s =>
{
var blob = s.Blob.GetBlob<LightningAddressDataBlob>();
return new EditLightningAddressVM.EditLightningAddressItem
{
Max = blob.Max,
Min = blob.Min,
CurrencyCode = blob.CurrencyCode,
StoreId = storeId,
Username = s.Username,
};
}
).ToList()
});
}
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[HttpPost("~/stores/{storeId}/integrations/lightning-address")]
@ -502,69 +518,71 @@ namespace BTCPayServer
{
if (command == "add")
{
if (!string.IsNullOrEmpty(vm.Add.CurrencyCode) && currencyNameTable.GetCurrencyData(vm.Add.CurrencyCode, false) is null)
if (!string.IsNullOrEmpty(vm.Add.CurrencyCode) &&
currencyNameTable.GetCurrencyData(vm.Add.CurrencyCode, false) is null)
{
vm.AddModelError(addressVm => addressVm.Add.CurrencyCode, "Currency is invalid", this);
}
if (!ModelState.IsValid)
{
return View(vm);
}
var lightningAddressSettings = await GetSettings();
if (lightningAddressSettings.Items.ContainsKey(vm.Add.Username.ToLowerInvariant()))
{
vm.AddModelError(addressVm => addressVm.Add.Username, "Username is already taken", this);
}
if (!ModelState.IsValid)
{
return View(vm);
}
if (lightningAddressSettings.StoreToItemMap.TryGetValue(storeId, out var ids))
if (await _lightningAddressService.Set(new LightningAddressData()
{
StoreDataId = storeId,
Username = vm.Add.Username,
Blob = new LightningAddressDataBlob()
{
Max = vm.Add.Max, Min = vm.Add.Min, CurrencyCode = vm.Add.CurrencyCode
}.SerializeBlob()
}))
{
ids = ids.Concat(new[] { vm.Add.Username.ToLowerInvariant() }).ToArray();
TempData.SetStatusMessageModel(new StatusMessageModel
{
Severity = StatusMessageModel.StatusSeverity.Success,
Message = "Lightning address added successfully."
});
}
else
{
ids = new[] { vm.Add.Username.ToLowerInvariant() };
vm.AddModelError(addressVm => addressVm.Add.Username, "Username is already taken", this);
if (!ModelState.IsValid)
{
return View(vm);
}
}
lightningAddressSettings.StoreToItemMap.AddOrReplace(storeId, ids);
vm.Add.StoreId = storeId;
lightningAddressSettings.Items.TryAdd(vm.Add.Username.ToLowerInvariant(), vm.Add);
await _settingsRepository.UpdateSetting(lightningAddressSettings, nameof(LightningAddressSettings));
TempData.SetStatusMessageModel(new StatusMessageModel
{
Severity = StatusMessageModel.StatusSeverity.Success,
Message = "Lightning address added successfully."
});
return RedirectToAction("EditLightningAddress");
}
if (command.StartsWith("remove", StringComparison.InvariantCultureIgnoreCase))
{
var lightningAddressSettings = await GetSettings();
var index = int.Parse(
command.Substring(command.IndexOf(":", StringComparison.InvariantCultureIgnoreCase) + 1),
CultureInfo.InvariantCulture);
if (lightningAddressSettings.StoreToItemMap.TryGetValue(storeId, out var addresses))
var index = command.Substring(command.IndexOf(":", StringComparison.InvariantCultureIgnoreCase) + 1);
if (await _lightningAddressService.Remove(index, storeId))
{
var addressToRemove = addresses[index];
addresses = addresses.Where(s => s != addressToRemove).ToArray();
lightningAddressSettings.StoreToItemMap.AddOrReplace(storeId, addresses);
lightningAddressSettings.Items.TryRemove(addressToRemove, out _);
await _settingsRepository.UpdateSetting(lightningAddressSettings, nameof(LightningAddressSettings));
TempData.SetStatusMessageModel(new StatusMessageModel
{
Severity = StatusMessageModel.StatusSeverity.Success,
Message = $"Lightning address {addressToRemove} removed successfully."
Message = $"Lightning address {index} removed successfully."
});
return RedirectToAction("EditLightningAddress");
}
else
{
vm.AddModelError(addressVm => addressVm.Add.Username, "Username could not be removed", this);
if (!ModelState.IsValid)
{
return View(vm);
}
}
}
return View(vm);
return RedirectToAction("EditLightningAddress");
}
}
}

View File

@ -120,15 +120,6 @@ namespace BTCPayServer.Controllers
return View(model);
}
[HttpGet]
public async Task<IActionResult> Generate(string version)
{
if (_env.NetworkType != NBitcoin.ChainName.Regtest)
return NotFound();
await _notificationSender.SendNotification(new AdminScope(), new NewVersionNotification(version));
return RedirectToAction(nameof(Index));
}
[HttpPost]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie, Policy = Policies.CanManageNotificationsForUser)]
public async Task<IActionResult> FlipRead(string id)

View File

@ -74,8 +74,7 @@ namespace BTCPayServer.Controllers
IncludeArchived = includeArchived
});
model.Total = result.Total;
model.Items = result.Items.Select(data => new ViewPaymentRequestViewModel(data)).ToList();
model.Items = result.Select(data => new ViewPaymentRequestViewModel(data)).ToList();
return View(model);
}
@ -186,7 +185,7 @@ namespace BTCPayServer.Controllers
{
if (redirectToInvoice)
{
return RedirectToAction("ViewPaymentRequest", new { Id = payReqId });
return RedirectToAction("ViewPaymentRequest", new { payReqId });
}
return BadRequest("Payment Request cannot be paid as it has been archived");
@ -197,7 +196,7 @@ namespace BTCPayServer.Controllers
{
if (redirectToInvoice)
{
return RedirectToAction("ViewPaymentRequest", new { Id = payReqId });
return RedirectToAction("ViewPaymentRequest", new { payReqId });
}
return BadRequest("Payment Request has already been settled.");
@ -207,7 +206,7 @@ namespace BTCPayServer.Controllers
{
if (redirectToInvoice)
{
return RedirectToAction("ViewPaymentRequest", new { Id = payReqId });
return RedirectToAction("ViewPaymentRequest", new { payReqId });
}
return BadRequest("Payment Request has expired");
@ -265,7 +264,7 @@ namespace BTCPayServer.Controllers
if (redirectToInvoice)
{
return RedirectToAction("Checkout", "UIInvoice", new { newInvoice.Id });
return RedirectToAction("Checkout", "UIInvoice", new { invoiceId = newInvoice.Id });
}
return Ok(newInvoice.Id);
@ -306,7 +305,7 @@ namespace BTCPayServer.Controllers
if (redirect)
{
TempData[WellKnownTempData.SuccessMessage] = "Payment cancelled";
return RedirectToAction(nameof(ViewPaymentRequest), new { Id = payReqId });
return RedirectToAction(nameof(ViewPaymentRequest), new { payReqId });
}
return Ok("Payment cancelled");

View File

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Extensions;
@ -162,7 +161,7 @@ namespace BTCPayServer.Controllers
{
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Message = $"Your claim request of {_currencyNameTable.DisplayFormatCurrency(vm.ClaimedAmount, ppBlob.Currency)} to {vm.Destination} has been submitted and is awaiting approval.",
Message = $"Your claim request of {_currencyNameTable.DisplayFormatCurrency(vm.ClaimedAmount, ppBlob.Currency)} to {vm.Destination} has been submitted and is awaiting {(result.PayoutData.State == PayoutState.AwaitingApproval? "approval": "payment")}.",
Severity = StatusMessageModel.StatusSeverity.Success
});
}

View File

@ -68,7 +68,6 @@ namespace BTCPayServer.Controllers
Disabled = u.LockoutEnabled && u.LockoutEnd != null && DateTimeOffset.UtcNow < u.LockoutEnd.Value.UtcDateTime
})
.ToListAsync();
model.Total = await usersQuery.CountAsync();
return View(model);
}

View File

@ -19,6 +19,8 @@ namespace BTCPayServer.Storage
public async Task<IActionResult> GetFile(string fileId)
{
var url = await _FileService.GetFileUrl(Request.GetAbsoluteRootUri(), fileId);
if (url is null)
return NotFound();
return new RedirectResult(url);
}
}

View File

@ -25,7 +25,6 @@ using StoreData = BTCPayServer.Data.StoreData;
namespace BTCPayServer.Controllers
{
[Route("stores/{storeId}/pull-payments")]
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[AutoValidateAntiforgeryToken]
public class UIStorePullPaymentsController : Controller
@ -44,6 +43,7 @@ namespace BTCPayServer.Controllers
return HttpContext.GetStoreData();
}
}
public UIStorePullPaymentsController(BTCPayNetworkProvider btcPayNetworkProvider,
IEnumerable<IPayoutHandler> payoutHandlers,
CurrencyNameTable currencyNameTable,
@ -59,7 +59,7 @@ namespace BTCPayServer.Controllers
_jsonSerializerSettings = jsonSerializerSettings;
}
[HttpGet("new")]
[HttpGet("stores/{storeId}/pull-payments/new")]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public async Task<IActionResult> NewPullPayment(string storeId)
{
@ -74,20 +74,21 @@ namespace BTCPayServer.Controllers
Message = "You must enable at least one payment method before creating a pull payment.",
Severity = StatusMessageModel.StatusSeverity.Error
});
return RedirectToAction(nameof(UIStoresController.GeneralSettings), "UIStores", new { storeId });
return RedirectToAction(nameof(UIStoresController.Dashboard), "UIStores", new { storeId });
}
return View(new NewPullPaymentModel
{
Name = "",
Currency = CurrentStore.GetStoreBlob().DefaultCurrency,
CustomCSSLink = "",
EmbeddedCSS = "",
PaymentMethodItems = paymentMethods.Select(id => new SelectListItem(id.ToPrettyString(), id.ToString(), true))
PaymentMethodItems =
paymentMethods.Select(id => new SelectListItem(id.ToPrettyString(), id.ToString(), true))
});
}
[HttpPost("new")]
[HttpPost("stores/{storeId}/pull-payments/new")]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public async Task<IActionResult> NewPullPayment(string storeId, NewPullPaymentModel model)
{
@ -104,7 +105,8 @@ namespace BTCPayServer.Controllers
{
// Since we assign all payment methods to be selected by default above we need to update
// them here to reflect user's selection so that they can correct their mistake
model.PaymentMethodItems = paymentMethodOptions.Select(id => new SelectListItem(id.ToPrettyString(), id.ToString(), false));
model.PaymentMethodItems =
paymentMethodOptions.Select(id => new SelectListItem(id.ToPrettyString(), id.ToString(), false));
ModelState.AddModelError(nameof(model.PaymentMethods), "You need at least one payment method");
}
if (_currencyNameTable.GetCurrencyData(model.Currency, false) is null)
@ -137,17 +139,17 @@ namespace BTCPayServer.Controllers
PaymentMethodIds = selectedPaymentMethodIds,
EmbeddedCSS = model.EmbeddedCSS,
CustomCSSLink = model.CustomCSSLink,
BOLT11Expiration = TimeSpan.FromDays(model.BOLT11Expiration)
BOLT11Expiration = TimeSpan.FromDays(model.BOLT11Expiration),
AutoApproveClaims = model.AutoApproveClaims
});
this.TempData.SetStatusMessageModel(new StatusMessageModel()
{
Message = "Pull payment request created",
Severity = StatusMessageModel.StatusSeverity.Success
Message = "Pull payment request created", Severity = StatusMessageModel.StatusSeverity.Success
});
return RedirectToAction(nameof(PullPayments), new { storeId = storeId });
}
[HttpGet("")]
[HttpGet("stores/{storeId}/pull-payments")]
public async Task<IActionResult> PullPayments(
string storeId,
PullPaymentState pullPaymentState,
@ -185,25 +187,23 @@ namespace BTCPayServer.Controllers
Message = "You must enable at least one payment method before creating a pull payment.",
Severity = StatusMessageModel.StatusSeverity.Error
});
return RedirectToAction(nameof(UIStoresController.GeneralSettings), "UIStores", new { storeId });
return RedirectToAction(nameof(UIStoresController.Dashboard), "UIStores", new { storeId });
}
var vm = this.ParseListQuery(new PullPaymentsModel
{
Skip = skip,
Count = count,
Total = await ppsQuery.CountAsync(),
ActiveState = pullPaymentState
Skip = skip, Count = count, ActiveState = pullPaymentState
});
switch (pullPaymentState) {
switch (pullPaymentState)
{
case PullPaymentState.Active:
ppsQuery = ppsQuery
.Where(
p => !p.Archived &&
(p.EndDate != null ? p.EndDate > DateTimeOffset.UtcNow : true) &&
p.StartDate <= DateTimeOffset.UtcNow
);
(p.EndDate != null ? p.EndDate > DateTimeOffset.UtcNow : true) &&
p.StartDate <= DateTimeOffset.UtcNow
);
break;
case PullPaymentState.Archived:
ppsQuery = ppsQuery.Where(p => p.Archived);
@ -225,10 +225,11 @@ namespace BTCPayServer.Controllers
{
var totalCompleted = pp.Payouts.Where(p => (p.State == PayoutState.Completed ||
p.State == PayoutState.InProgress) && p.IsInPeriod(pp, now))
.Select(o => o.GetBlob(_jsonSerializerSettings).Amount).Sum();
.Select(o => o.GetBlob(_jsonSerializerSettings).Amount).Sum();
var totalAwaiting = pp.Payouts.Where(p => (p.State == PayoutState.AwaitingPayment ||
p.State == PayoutState.AwaitingApproval) &&
p.IsInPeriod(pp, now)).Select(o => o.GetBlob(_jsonSerializerSettings).Amount).Sum();
p.IsInPeriod(pp, now)).Select(o =>
o.GetBlob(_jsonSerializerSettings).Amount).Sum();
;
var ppBlob = pp.GetBlob();
var ni = _currencyNameTable.GetCurrencyData(ppBlob.Currency, true);
@ -262,15 +263,16 @@ namespace BTCPayServer.Controllers
return time;
}
[HttpGet("{pullPaymentId}/archive")]
[HttpGet("stores/{storeId}/pull-payments/{pullPaymentId}/archive")]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public IActionResult ArchivePullPayment(string storeId,
string pullPaymentId)
{
return View("Confirm", new ConfirmModel("Archive pull payment", "Do you really want to archive the pull payment?", "Archive"));
return View("Confirm",
new ConfirmModel("Archive pull payment", "Do you really want to archive the pull payment?", "Archive"));
}
[HttpPost("{pullPaymentId}/archive")]
[HttpPost("stores/{storeId}/pull-payments/{pullPaymentId}/archive")]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public async Task<IActionResult> ArchivePullPaymentPost(string storeId,
string pullPaymentId)
@ -278,14 +280,15 @@ namespace BTCPayServer.Controllers
await _pullPaymentService.Cancel(new HostedServices.PullPaymentHostedService.CancelRequest(pullPaymentId));
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Message = "Pull payment archived",
Severity = StatusMessageModel.StatusSeverity.Success
Message = "Pull payment archived", Severity = StatusMessageModel.StatusSeverity.Success
});
return RedirectToAction(nameof(PullPayments), new { storeId });
}
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[HttpPost("payouts")]
[HttpPost("stores/{storeId}/pull-payments/payouts")]
[HttpPost("stores/{storeId}/pull-payments/{pullPaymentId}/payouts")]
[HttpPost("stores/{storeId}/payouts")]
public async Task<IActionResult> PayoutsPost(
string storeId, PayoutsModel vm, CancellationToken cancellationToken)
{
@ -302,16 +305,17 @@ namespace BTCPayServer.Controllers
{
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Message = "No payout selected",
Severity = StatusMessageModel.StatusSeverity.Error
});
return RedirectToAction(nameof(Payouts), new
{
storeId = storeId,
pullPaymentId = vm.PullPaymentId,
paymentMethodId = paymentMethodId.ToString()
Message = "No payout selected", Severity = StatusMessageModel.StatusSeverity.Error
});
return RedirectToAction(nameof(Payouts),
new
{
storeId = storeId,
pullPaymentId = vm.PullPaymentId,
paymentMethodId = paymentMethodId.ToString()
});
}
var command = vm.Command.Substring(vm.Command.IndexOf('-', StringComparison.InvariantCulture) + 1);
if (handler != null)
{
@ -321,124 +325,127 @@ namespace BTCPayServer.Controllers
TempData.SetStatusMessageModel(result);
}
}
switch (command)
{
case "approve-pay":
case "approve":
{
await using var ctx = this._dbContextFactory.CreateContext();
ctx.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
var payouts = await GetPayoutsForPaymentMethod(paymentMethodId, ctx, payoutIds, storeId, cancellationToken);
{
await using var ctx = this._dbContextFactory.CreateContext();
ctx.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
var payouts =
await GetPayoutsForPaymentMethod(paymentMethodId, ctx, payoutIds, storeId, cancellationToken);
var failed = false;
for (int i = 0; i < payouts.Count; i++)
var failed = false;
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)
{
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()
{
this.TempData.SetStatusMessageModel(new StatusMessageModel()
{
Message = $"Rate unavailable: {rateResult.EvaluatedRule}",
Severity = StatusMessageModel.StatusSeverity.Error
});
failed = true;
break;
}
var approveResult = await _pullPaymentService.Approve(new HostedServices.PullPaymentHostedService.PayoutApproval()
Message = $"Rate unavailable: {rateResult.EvaluatedRule}",
Severity = StatusMessageModel.StatusSeverity.Error
});
failed = true;
break;
}
var approveResult = await _pullPaymentService.Approve(
new HostedServices.PullPaymentHostedService.PayoutApproval()
{
PayoutId = payout.Id,
Revision = payout.GetBlob(_jsonSerializerSettings).Revision,
Rate = rateResult.BidAsk.Ask
});
if (approveResult != PullPaymentHostedService.PayoutApproval.Result.Ok)
{
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Message = PullPaymentHostedService.PayoutApproval.GetErrorMessage(approveResult),
Severity = StatusMessageModel.StatusSeverity.Error
});
failed = true;
break;
}
}
if (failed)
if (approveResult != PullPaymentHostedService.PayoutApproval.Result.Ok)
{
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Message = PullPaymentHostedService.PayoutApproval.GetErrorMessage(approveResult),
Severity = StatusMessageModel.StatusSeverity.Error
});
failed = true;
break;
}
if (command == "approve-pay")
{
goto case "pay";
}
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Message = "Payouts approved",
Severity = StatusMessageModel.StatusSeverity.Success
});
}
if (failed)
{
break;
}
if (command == "approve-pay")
{
goto case "pay";
}
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Message = "Payouts approved", Severity = StatusMessageModel.StatusSeverity.Success
});
break;
}
case "pay":
{
if (handler is { })
return await handler?.InitiatePayment(paymentMethodId, payoutIds);
TempData.SetStatusMessageModel(new StatusMessageModel()
{
if (handler is { })
return await handler?.InitiatePayment(paymentMethodId, payoutIds);
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Message = "Paying via this payment method is not supported",
Severity = StatusMessageModel.StatusSeverity.Error
});
break;
}
Message = "Paying via this payment method is not supported",
Severity = StatusMessageModel.StatusSeverity.Error
});
break;
}
case "mark-paid":
{
await using var ctx = this._dbContextFactory.CreateContext();
ctx.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
var payouts =
await GetPayoutsForPaymentMethod(paymentMethodId, ctx, payoutIds, storeId, cancellationToken);
for (int i = 0; i < payouts.Count; i++)
{
await using var ctx = this._dbContextFactory.CreateContext();
ctx.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
var payouts = await GetPayoutsForPaymentMethod(paymentMethodId, ctx, payoutIds, storeId, cancellationToken);
for (int i = 0; i < payouts.Count; i++)
{
var payout = payouts[i];
if (payout.State != PayoutState.AwaitingPayment)
continue;
var payout = payouts[i];
if (payout.State != PayoutState.AwaitingPayment)
continue;
var result = await _pullPaymentService.MarkPaid(new PayoutPaidRequest()
var result =
await _pullPaymentService.MarkPaid(new PayoutPaidRequest() { PayoutId = payout.Id });
if (result != PayoutPaidRequest.PayoutPaidResult.Ok)
{
TempData.SetStatusMessageModel(new StatusMessageModel()
{
PayoutId = payout.Id
Message = PayoutPaidRequest.GetErrorMessage(result),
Severity = StatusMessageModel.StatusSeverity.Error
});
if (result != PayoutPaidRequest.PayoutPaidResult.Ok)
{
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Message = PayoutPaidRequest.GetErrorMessage(result),
Severity = StatusMessageModel.StatusSeverity.Error
});
return RedirectToAction(nameof(Payouts), new
return RedirectToAction(nameof(Payouts),
new
{
storeId = storeId,
pullPaymentId = vm.PullPaymentId,
paymentMethodId = paymentMethodId.ToString()
});
}
}
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Message = "Payouts marked as paid",
Severity = StatusMessageModel.StatusSeverity.Success
});
break;
}
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Message = "Payouts marked as paid", Severity = StatusMessageModel.StatusSeverity.Success
});
break;
}
case "cancel":
await _pullPaymentService.Cancel(
new PullPaymentHostedService.CancelRequest(payoutIds));
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Message = "Payouts archived",
Severity = StatusMessageModel.StatusSeverity.Success
Message = "Payouts archived", Severity = StatusMessageModel.StatusSeverity.Success
});
break;
}
@ -458,16 +465,17 @@ namespace BTCPayServer.Controllers
{
var payouts = (await ctx.Payouts
.Include(p => p.PullPaymentData)
.Include(p => p.PullPaymentData.StoreData)
.Include(p => p.StoreData)
.Where(p => payoutIds.Contains(p.Id))
.Where(p => p.PullPaymentData.StoreId == storeId && !p.PullPaymentData.Archived)
.Where(p => p.StoreDataId == storeId && (p.PullPaymentDataId == null || !p.PullPaymentData.Archived))
.ToListAsync(cancellationToken))
.Where(p => p.GetPaymentMethodId() == paymentMethodId)
.ToList();
return payouts;
}
[HttpGet("payouts")]
[HttpGet("stores/{storeId}/pull-payments/{pullPaymentId}/payouts")]
[HttpGet("stores/{storeId}/payouts")]
public async Task<IActionResult> Payouts(
string storeId, string pullPaymentId, string paymentMethodId, PayoutState payoutState,
int skip = 0, int count = 50)
@ -480,7 +488,7 @@ namespace BTCPayServer.Controllers
Message = "You must enable at least one payment method before creating a payout.",
Severity = StatusMessageModel.StatusSeverity.Error
});
return RedirectToAction(nameof(UIStoresController.GeneralSettings), "UIStores", new { storeId });
return RedirectToAction(nameof(UIStoresController.Dashboard), "UIStores", new { storeId });
}
var vm = this.ParseListQuery(new PayoutsModel
@ -494,7 +502,8 @@ namespace BTCPayServer.Controllers
});
vm.Payouts = new List<PayoutsModel.PayoutModel>();
await using var ctx = _dbContextFactory.CreateContext();
var payoutRequest = ctx.Payouts.Where(p => p.PullPaymentData.StoreId == storeId && !p.PullPaymentData.Archived);
var payoutRequest =
ctx.Payouts.Where(p => p.StoreDataId == storeId && (p.PullPaymentDataId == null || !p.PullPaymentData.Archived));
if (pullPaymentId != null)
{
payoutRequest = payoutRequest.Where(p => p.PullPaymentDataId == vm.PullPaymentId);
@ -507,6 +516,9 @@ namespace BTCPayServer.Controllers
payoutRequest = payoutRequest.Where(p => p.PaymentMethodId == pmiStr);
}
vm.PaymentMethodCount = (await payoutRequest.GroupBy(data => data.PaymentMethodId)
.Select(datas => new {datas.Key, Count = datas.Count()}).ToListAsync())
.ToDictionary(datas => datas.Key, arg => arg.Count);
vm.PayoutStateCount = payoutRequest.GroupBy(data => data.State)
.Select(e => new { e.Key, Count = e.Count() })
.ToDictionary(arg => arg.Key, arg => arg.Count);
@ -521,26 +533,22 @@ namespace BTCPayServer.Controllers
.ToDictionary(pair => pair.Key, pair => pair.Value);
payoutRequest = payoutRequest.Where(p => p.State == vm.PayoutState);
vm.Total = await payoutRequest.CountAsync();
payoutRequest = payoutRequest.Skip(vm.Skip).Take(vm.Count);
var payouts = await payoutRequest.OrderByDescending(p => p.Date)
.Select(o => new
{
Payout = o,
PullPayment = o.PullPaymentData
}).ToListAsync();
.Select(o => new { Payout = o, PullPayment = o.PullPaymentData }).ToListAsync();
foreach (var item in payouts)
{
var ppBlob = item.PullPayment.GetBlob();
var ppBlob = item.PullPayment?.GetBlob();
var payoutBlob = item.Payout.GetBlob(_jsonSerializerSettings);
var m = new PayoutsModel.PayoutModel
{
PullPaymentId = item.PullPayment.Id,
PullPaymentName = ppBlob.Name ?? item.PullPayment.Id,
PullPaymentId = item.PullPayment?.Id,
PullPaymentName = ppBlob?.Name ?? item.PullPayment?.Id,
Date = item.Payout.Date,
PayoutId = item.Payout.Id,
Amount = _currencyNameTable.DisplayFormatCurrency(payoutBlob.Amount, ppBlob.Currency),
Amount = _currencyNameTable.DisplayFormatCurrency(payoutBlob.Amount, ppBlob?.Currency ?? PaymentMethodId.Parse(item.Payout.PaymentMethodId).CryptoCode),
Destination = payoutBlob.Destination
};
var handler = _payoutHandlers

View File

@ -49,13 +49,24 @@ namespace BTCPayServer.Controllers
{
var services = _externalServiceOptions.Value.ExternalServices.ToList()
.Where(service => _externalServiceTypes.Contains(service.Type))
.Select(async service => new AdditionalServiceViewModel
.Select(async service =>
{
DisplayName = service.DisplayName,
ServiceName = service.ServiceName,
CryptoCode = service.CryptoCode,
Type = service.Type.ToString(),
Link = await GetServiceLink(service)
var model = new AdditionalServiceViewModel
{
DisplayName = service.DisplayName,
ServiceName = service.ServiceName,
CryptoCode = service.CryptoCode,
Type = service.Type.ToString()
};
try
{
model.Link = await GetServiceLink(service);
}
catch (Exception exception)
{
model.Error = exception.Message;
}
return model;
})
.Select(t => t.Result)
.ToList();

View File

@ -464,8 +464,8 @@ namespace BTCPayServer.Controllers
var defaultChoice = defaultPaymentId is not null ? defaultPaymentId.FindNearest(enabled) : null;
if (defaultChoice is null)
{
defaultChoice = enabled.FirstOrDefault(e => e.CryptoCode == "BTC" && e.PaymentType == PaymentTypes.BTCLike) ??
enabled.FirstOrDefault(e => e.CryptoCode == "BTC" && e.PaymentType == PaymentTypes.LightningLike) ??
defaultChoice = enabled.FirstOrDefault(e => e.CryptoCode == _NetworkProvider.DefaultNetwork.CryptoCode && e.PaymentType == PaymentTypes.BTCLike) ??
enabled.FirstOrDefault(e => e.CryptoCode == _NetworkProvider.DefaultNetwork.CryptoCode && e.PaymentType == PaymentTypes.LightningLike) ??
enabled.FirstOrDefault();
}
var choices = GetEnabledPaymentMethodChoices(storeData);

View File

@ -13,7 +13,6 @@ using BTCPayServer.Data;
using BTCPayServer.HostedServices;
using BTCPayServer.ModelBinders;
using BTCPayServer.Models;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Models.WalletViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Payments.PayJoin;
@ -28,6 +27,8 @@ using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using NBitcoin;
using BTCPayServer.Client.Models;
using BTCPayServer.Logging;
using NBXplorer;
using NBXplorer.Client;
using NBXplorer.DerivationStrategy;
@ -50,7 +51,6 @@ namespace BTCPayServer.Controllers
public RateFetcher RateFetcher { get; }
private readonly UserManager<ApplicationUser> _userManager;
private readonly JsonSerializerSettings _serializerSettings;
private readonly NBXplorerDashboard _dashboard;
private readonly IAuthorizationService _authorizationService;
private readonly IFeeProviderFactory _feeRateProvider;
@ -61,6 +61,7 @@ namespace BTCPayServer.Controllers
private readonly DelayedTransactionBroadcaster _broadcaster;
private readonly PayjoinClient _payjoinClient;
private readonly LabelFactory _labelFactory;
private readonly PullPaymentHostedService _pullPaymentHostedService;
private readonly ApplicationDbContextFactory _dbContextFactory;
private readonly BTCPayNetworkJsonSerializerSettings _jsonSerializerSettings;
private readonly PullPaymentHostedService _pullPaymentService;
@ -69,6 +70,7 @@ namespace BTCPayServer.Controllers
private readonly WalletHistogramService _walletHistogramService;
readonly CurrencyNameTable _currencyTable;
public UIWalletsController(StoreRepository repo,
WalletRepository walletRepository,
CurrencyNameTable currencyTable,
@ -93,7 +95,8 @@ namespace BTCPayServer.Controllers
BTCPayNetworkJsonSerializerSettings jsonSerializerSettings,
PullPaymentHostedService pullPaymentService,
IEnumerable<IPayoutHandler> payoutHandlers,
IServiceProvider serviceProvider)
IServiceProvider serviceProvider,
PullPaymentHostedService pullPaymentHostedService)
{
_currencyTable = currencyTable;
Repository = repo;
@ -102,7 +105,6 @@ namespace BTCPayServer.Controllers
_authorizationService = authorizationService;
NetworkProvider = networkProvider;
_userManager = userManager;
_serializerSettings = mvcJsonOptions.SerializerSettings;
_dashboard = dashboard;
ExplorerClientProvider = explorerProvider;
_feeRateProvider = feeRateProvider;
@ -113,6 +115,7 @@ namespace BTCPayServer.Controllers
_broadcaster = broadcaster;
_payjoinClient = payjoinClient;
_labelFactory = labelFactory;
_pullPaymentHostedService = pullPaymentHostedService;
_dbContextFactory = dbContextFactory;
_jsonSerializerSettings = jsonSerializerSettings;
_pullPaymentService = pullPaymentService;
@ -121,7 +124,7 @@ namespace BTCPayServer.Controllers
_connectionFactory = connectionFactory;
_walletHistogramService = walletHistogramService;
}
[HttpPost]
[Route("{walletId}")]
public async Task<IActionResult> ModifyTransaction(
@ -130,10 +133,10 @@ namespace BTCPayServer.Controllers
// does not work
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, string transactionId,
string addlabel = null,
string addlabelclick = null,
string addcomment = null,
string removelabel = null)
string addlabel = null,
string addlabelclick = null,
string addcomment = null,
string removelabel = null)
{
addlabel = addlabel ?? addlabelclick;
// Hack necessary when the user enter a empty comment and submit.
@ -184,7 +187,8 @@ namespace BTCPayServer.Controllers
{
if (walletTransactionInfo.Labels.Remove(removelabel))
{
var canDeleteColor = !walletTransactionsInfo.Any(txi => txi.Value.Labels.ContainsKey(removelabel));
var canDeleteColor =
!walletTransactionsInfo.Any(txi => txi.Value.Labels.ContainsKey(removelabel));
if (canDeleteColor)
{
walletBlobInfo.LabelColors.Remove(removelabel);
@ -219,18 +223,18 @@ namespace BTCPayServer.Controllers
var stores = await Repository.GetStoresByUserId(GetUserId());
var onChainWallets = stores
.SelectMany(s => s.GetSupportedPaymentMethods(NetworkProvider)
.OfType<DerivationSchemeSettings>()
.Select(d => ((Wallet: _walletProvider.GetWallet(d.Network),
DerivationStrategy: d.AccountDerivation,
Network: d.Network)))
.Where(_ => _.Wallet != null && _.Network.WalletSupported)
.Select(_ => (Wallet: _.Wallet,
Store: s,
Balance: GetBalanceString(_.Wallet, _.DerivationStrategy),
DerivationStrategy: _.DerivationStrategy,
Network: _.Network)))
.ToList();
.SelectMany(s => s.GetSupportedPaymentMethods(NetworkProvider)
.OfType<DerivationSchemeSettings>()
.Select(d => ((Wallet: _walletProvider.GetWallet(d.Network),
DerivationStrategy: d.AccountDerivation,
Network: d.Network)))
.Where(_ => _.Wallet != null && _.Network.WalletSupported)
.Select(_ => (Wallet: _.Wallet,
Store: s,
Balance: GetBalanceString(_.Wallet, _.DerivationStrategy),
DerivationStrategy: _.DerivationStrategy,
Network: _.Network)))
.ToList();
foreach (var wallet in onChainWallets)
{
@ -242,6 +246,7 @@ namespace BTCPayServer.Controllers
{
walletVm.Balance = "";
}
walletVm.CryptoCode = wallet.Network.CryptoCode;
walletVm.StoreId = wallet.Store.Id;
walletVm.Id = new WalletId(wallet.Store.Id, wallet.Network.CryptoCode);
@ -276,18 +281,10 @@ namespace BTCPayServer.Controllers
var transactions = await wallet.FetchTransactions(paymentMethod.AccountDerivation);
var walletBlob = await walletBlobAsync;
var walletTransactionsInfo = await walletTransactionsInfoAsync;
var model = new ListTransactionsViewModel
{
Skip = skip,
Count = count,
Total = 0
};
var model = new ListTransactionsViewModel { Skip = skip, Count = count };
if (labelFilter != null)
{
model.PaginationQuery = new Dictionary<string, object>
{
{"labelFilter", labelFilter}
};
model.PaginationQuery = new Dictionary<string, object> { { "labelFilter", labelFilter } };
}
if (transactions == null)
{
@ -302,7 +299,7 @@ namespace BTCPayServer.Controllers
else
{
foreach (var tx in transactions.UnconfirmedTransactions.Transactions
.Concat(transactions.ConfirmedTransactions.Transactions).ToArray())
.Concat(transactions.ConfirmedTransactions.Transactions).ToArray())
{
var vm = new ListTransactionsViewModel.TransactionViewModel();
vm.Id = tx.TransactionId.ToString();
@ -327,7 +324,8 @@ namespace BTCPayServer.Controllers
}
model.Total = model.Transactions.Count;
model.Transactions = model.Transactions.OrderByDescending(t => t.Timestamp).Skip(skip).Take(count).ToList();
model.Transactions = model.Transactions.OrderByDescending(t => t.Timestamp).Skip(skip).Take(count)
.ToList();
}
model.CryptoCode = walletId.CryptoCode;
@ -354,8 +352,7 @@ namespace BTCPayServer.Controllers
}
[HttpGet("{walletId}/receive")]
public IActionResult WalletReceive([ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId)
public IActionResult WalletReceive([ModelBinder(typeof(WalletIdModelBinder))] WalletId walletId)
{
if (walletId?.StoreId == null)
return NotFound();
@ -371,7 +368,9 @@ namespace BTCPayServer.Controllers
var bip21 = network.GenerateBIP21(address?.ToString(), null);
if (allowedPayjoin)
{
bip21.QueryParams.Add(PayjoinClient.BIP21EndpointKey, Request.GetAbsoluteUri(Url.Action(nameof(PayJoinEndpointController.Submit), "PayJoinEndpoint", new { walletId.CryptoCode })));
bip21.QueryParams.Add(PayjoinClient.BIP21EndpointKey,
Request.GetAbsoluteUri(Url.Action(nameof(PayJoinEndpointController.Submit), "PayJoinEndpoint",
new { walletId.CryptoCode })));
}
return View(new WalletReceiveViewModel()
{
@ -384,8 +383,8 @@ namespace BTCPayServer.Controllers
[HttpPost]
[Route("{walletId}/receive")]
public async Task<IActionResult> WalletReceive([ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, WalletReceiveViewModel viewModel, string command)
public async Task<IActionResult> WalletReceive([ModelBinder(typeof(WalletIdModelBinder))] WalletId walletId,
WalletReceiveViewModel viewModel, string command)
{
if (walletId?.StoreId == null)
return NotFound();
@ -479,17 +478,13 @@ namespace BTCPayServer.Controllers
rateRules.Spread = 0.0m;
var currencyPair = new Rating.CurrencyPair(paymentMethod.PaymentId.CryptoCode, storeData.DefaultCurrency);
double.TryParse(defaultAmount, out var amount);
var model = new WalletSendModel()
{
CryptoCode = walletId.CryptoCode
};
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);
}
}
@ -517,7 +512,10 @@ namespace BTCPayServer.Controllers
{
var result = await feeProvider.GetFeeRateAsync(
(int)network.NBitcoinNetwork.Consensus.GetExpectedBlocksFor(time));
return new WalletSendModel.FeeRateOption() { Target = time, FeeRate = result.SatoshiPerByte };
return new WalletSendModel.FeeRateOption()
{
Target = time, FeeRate = result.SatoshiPerByte
};
}
catch (Exception)
{
@ -547,37 +545,46 @@ namespace BTCPayServer.Controllers
try
{
cts.CancelAfter(TimeSpan.FromSeconds(5));
var result = await RateFetcher.FetchRate(currencyPair, rateRules, cts.Token).WithCancellation(cts.Token);
var result = await RateFetcher.FetchRate(currencyPair, rateRules, cts.Token)
.WithCancellation(cts.Token);
if (result.BidAsk != null)
{
model.Rate = result.BidAsk.Center;
model.FiatDivisibility = _currencyTable.GetNumberFormatInfo(currencyPair.Right, true).CurrencyDecimalDigits;
model.FiatDivisibility = _currencyTable.GetNumberFormatInfo(currencyPair.Right, true)
.CurrencyDecimalDigits;
model.Fiat = currencyPair.Right;
}
else
{
model.RateError = $"{result.EvaluatedRule} ({string.Join(", ", result.Errors.OfType<object>().ToArray())})";
model.RateError =
$"{result.EvaluatedRule} ({string.Join(", ", result.Errors.OfType<object>().ToArray())})";
}
}
catch (Exception ex) { model.RateError = ex.Message; }
}
return View(model);
}
private async Task<string> GetSeed(WalletId walletId, BTCPayNetwork network)
{
return await CanUseHotWallet() &&
GetDerivationSchemeSettings(walletId) is DerivationSchemeSettings s &&
s.IsHotWallet &&
ExplorerClientProvider.GetExplorerClient(network) is ExplorerClient client &&
await client.GetMetadataAsync<string>(s.AccountDerivation, WellknownMetadataKeys.MasterHDKey) is string seed &&
!string.IsNullOrEmpty(seed) ? seed : null;
GetDerivationSchemeSettings(walletId) is DerivationSchemeSettings s &&
s.IsHotWallet &&
ExplorerClientProvider.GetExplorerClient(network) is ExplorerClient client &&
await client.GetMetadataAsync<string>(s.AccountDerivation, WellknownMetadataKeys.MasterHDKey) is
string seed &&
!string.IsNullOrEmpty(seed)
? seed
: null;
}
[HttpPost("{walletId}/send")]
public async Task<IActionResult> WalletSend(
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, WalletSendModel vm, string command = "", CancellationToken cancellation = default, string bip21 = "")
WalletId walletId, WalletSendModel vm, string command = "", CancellationToken cancellation = default,
string bip21 = "")
{
if (walletId?.StoreId == null)
return NotFound();
@ -606,7 +613,8 @@ namespace BTCPayServer.Controllers
var walletBlobAsync = await WalletRepository.GetWalletInfo(walletId);
var walletTransactionsInfoAsync = await WalletRepository.GetWalletTransactionsInfo(walletId);
var utxos = await _walletProvider.GetWallet(network).GetUnspentCoins(schemeSettings.AccountDerivation, cancellation);
var utxos = await _walletProvider.GetWallet(network)
.GetUnspentCoins(schemeSettings.AccountDerivation, cancellation);
vm.InputsAvailable = utxos.Select(coin =>
{
walletTransactionsInfoAsync.TryGetValue(coin.OutPoint.Hash.ToString(), out var info);
@ -615,8 +623,12 @@ namespace BTCPayServer.Controllers
Outpoint = coin.OutPoint.ToString(),
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()),
Labels =
info == null
? null
: _labelFactory.ColorizeTransactionLabels(walletBlobAsync, info, Request),
Link = string.Format(CultureInfo.InvariantCulture, network.BlockExplorerLink,
coin.OutPoint.Hash.ToString()),
Confirmations = coin.Confirmations
};
}).ToArray();
@ -645,7 +657,9 @@ namespace BTCPayServer.Controllers
if (command.StartsWith("remove-output", StringComparison.InvariantCultureIgnoreCase))
{
ModelState.Clear();
var index = int.Parse(command.Substring(command.IndexOf(":", StringComparison.InvariantCultureIgnoreCase) + 1), CultureInfo.InvariantCulture);
var index = int.Parse(
command.Substring(command.IndexOf(":", StringComparison.InvariantCultureIgnoreCase) + 1),
CultureInfo.InvariantCulture);
vm.Outputs.RemoveAt(index);
return View(vm);
}
@ -657,6 +671,8 @@ namespace BTCPayServer.Controllers
return View(vm);
}
var bypassBalanceChecks = command == "schedule";
var subtractFeesOutputsCount = new List<int>();
var substractFees = vm.Outputs.Any(o => o.SubtractFeesFromOutput);
for (var i = 0; i < vm.Outputs.Count; i++)
@ -669,17 +685,20 @@ namespace BTCPayServer.Controllers
transactionOutput.DestinationAddress = transactionOutput.DestinationAddress?.Trim() ?? string.Empty;
var inputName =
string.Format(CultureInfo.InvariantCulture, "Outputs[{0}].", i.ToString(CultureInfo.InvariantCulture)) +
nameof(transactionOutput.DestinationAddress);
string.Format(CultureInfo.InvariantCulture, "Outputs[{0}].",
i.ToString(CultureInfo.InvariantCulture)) +
nameof(transactionOutput.DestinationAddress);
try
{
var address = BitcoinAddress.Create(transactionOutput.DestinationAddress, network.NBitcoinNetwork);
if (address is TaprootAddress)
{
var supportTaproot = _dashboard.Get(network.CryptoCode)?.Status?.BitcoinStatus?.Capabilities?.CanSupportTaproot;
var supportTaproot = _dashboard.Get(network.CryptoCode)?.Status?.BitcoinStatus?.Capabilities
?.CanSupportTaproot;
if (!(supportTaproot is true))
{
ModelState.AddModelError(inputName, "You need to update your full node, and/or NBXplorer (Version >= 2.1.56) to be able to send to a taproot address.");
ModelState.AddModelError(inputName,
"You need to update your full node, and/or NBXplorer (Version >= 2.1.56) to be able to send to a taproot address.");
}
}
}
@ -688,7 +707,7 @@ namespace BTCPayServer.Controllers
ModelState.AddModelError(inputName, "Invalid address");
}
if (transactionOutput.Amount.HasValue)
if (!bypassBalanceChecks && transactionOutput.Amount.HasValue)
{
transactionAmountSum += transactionOutput.Amount.Value;
@ -700,41 +719,120 @@ namespace BTCPayServer.Controllers
}
}
if (subtractFeesOutputsCount.Count > 1)
if (!bypassBalanceChecks)
{
foreach (var subtractFeesOutput in subtractFeesOutputsCount)
if (subtractFeesOutputsCount.Count > 1)
{
vm.AddModelError(model => model.Outputs[subtractFeesOutput].SubtractFeesFromOutput,
"You can only subtract fees from one output", this);
foreach (var subtractFeesOutput in subtractFeesOutputsCount)
{
vm.AddModelError(model => model.Outputs[subtractFeesOutput].SubtractFeesFromOutput,
"You can only subtract fees from one output", this);
}
}
else if (vm.CurrentBalance == transactionAmountSum && !substractFees)
{
ModelState.AddModelError(string.Empty,
"You are sending your entire balance, you should subtract the fees from an output");
}
if (vm.CurrentBalance < transactionAmountSum)
{
for (var i = 0; i < vm.Outputs.Count; i++)
{
vm.AddModelError(model => model.Outputs[i].Amount,
"You are sending more than what you own", this);
}
}
if (vm.FeeSatoshiPerByte is decimal fee)
{
if (fee < 0)
{
vm.AddModelError(model => model.FeeSatoshiPerByte,
"The fee rate should be above 0", this);
}
}
}
else if (vm.CurrentBalance == transactionAmountSum && !substractFees)
{
ModelState.AddModelError(string.Empty,
"You are sending your entire balance, you should subtract the fees from an output");
}
if (vm.CurrentBalance < transactionAmountSum)
{
for (var i = 0; i < vm.Outputs.Count; i++)
{
vm.AddModelError(model => model.Outputs[i].Amount,
"You are sending more than what you own", this);
}
}
if (vm.FeeSatoshiPerByte is decimal fee)
{
if (fee < 0)
{
vm.AddModelError(model => model.FeeSatoshiPerByte,
"The fee rate should be above 0", this);
}
}
if (!ModelState.IsValid)
return View(vm);
DerivationSchemeSettings derivationScheme = GetDerivationSchemeSettings(walletId);
CreatePSBTResponse psbtResponse;
if (command == "schedule")
{
var pmi = new PaymentMethodId(walletId.CryptoCode, BitcoinPaymentType.Instance);
var claims =
vm.Outputs.Where(output => string.IsNullOrEmpty(output.PayoutId)).Select(output => new ClaimRequest()
{
Destination = new AddressClaimDestination(
BitcoinAddress.Create(output.DestinationAddress, network.NBitcoinNetwork)),
Value = output.Amount.Value,
PaymentMethodId = pmi,
StoreId = walletId.StoreId,
PreApprove = true,
}).ToArray();
var someFailed = false;
string message = null;
string errorMessage = null;
var result = new Dictionary<ClaimRequest, ClaimRequest.ClaimResult>();
foreach (ClaimRequest claimRequest in claims)
{
var response = await _pullPaymentHostedService.Claim(claimRequest);
result.Add(claimRequest, response.Result);
if (response.Result == ClaimRequest.ClaimResult.Ok)
{
if (message is null)
{
message = "Payouts scheduled:<br/>";
}
message += $"{claimRequest.Value} to {claimRequest.Destination.ToString()}<br/>";
}
else
{
someFailed = true;
if (errorMessage is null)
{
errorMessage = "Payouts failed to be scheduled:<br/>";
}
switch (response.Result)
{
case ClaimRequest.ClaimResult.Duplicate:
errorMessage += $"{claimRequest.Value} to {claimRequest.Destination.ToString() } - address reuse<br/>";
break;
case ClaimRequest.ClaimResult.AmountTooLow:
errorMessage += $"{claimRequest.Value} to {claimRequest.Destination.ToString() } - amount too low<br/>";
break;
}
}
}
if (message is not null && errorMessage is not null)
{
message += $"<br/><br/>{errorMessage}";
}
else if(message is null && errorMessage is not null)
{
message = errorMessage;
}
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Severity =someFailed? StatusMessageModel.StatusSeverity.Warning:
StatusMessageModel.StatusSeverity.Success,
Html = message
});
return RedirectToAction("Payouts", "UIStorePullPayments",
new
{
storeId = walletId.StoreId,
PaymentMethodId = pmi.ToString(),
payoutState = PayoutState.AwaitingPayment,
});
}
try
{
psbtResponse = await CreatePSBT(network, derivationScheme, vm, cancellation);
@ -763,23 +861,17 @@ namespace BTCPayServer.Controllers
switch (command)
{
case "sign":
return await WalletSign(walletId, new WalletPSBTViewModel()
{
SigningContext = signingContext
});
return await WalletSign(walletId, new WalletPSBTViewModel() { SigningContext = signingContext });
case "analyze-psbt":
var name =
$"Send-{string.Join('_', vm.Outputs.Select(output => $"{output.Amount}->{output.DestinationAddress}{(output.SubtractFeesFromOutput ? "-Fees" : string.Empty)}"))}.psbt";
return RedirectToWalletPSBT(new WalletPSBTViewModel
{
PSBT = psbt.ToBase64(),
FileName = name
});
return RedirectToWalletPSBT(new WalletPSBTViewModel { PSBT = psbt.ToBase64(), FileName = name });
default:
return View(vm);
}
}
private void LoadFromBIP21(WalletSendModel vm, string bip21, BTCPayNetwork network)
{
vm.Outputs ??= new List<WalletSendModel.TransactionOutput>();
@ -791,7 +883,10 @@ namespace BTCPayServer.Controllers
{
Amount = uriBuilder.Amount?.ToDecimal(MoneyUnit.BTC),
DestinationAddress = uriBuilder.Address.ToString(),
SubtractFeesFromOutput = false
SubtractFeesFromOutput = false,
PayoutId = uriBuilder.UnknownParameters.ContainsKey("payout")
? uriBuilder.UnknownParameters["payout"]
: null
});
if (!string.IsNullOrEmpty(uriBuilder.Label) || !string.IsNullOrEmpty(uriBuilder.Message))
{
@ -811,9 +906,9 @@ namespace BTCPayServer.Controllers
try
{
vm.Outputs.Add(new WalletSendModel.TransactionOutput()
{
DestinationAddress = BitcoinAddress.Create(bip21, network.NBitcoinNetwork).ToString()
}
{
DestinationAddress = BitcoinAddress.Create(bip21, network.NBitcoinNetwork).ToString()
}
);
}
catch
@ -831,23 +926,22 @@ namespace BTCPayServer.Controllers
private IActionResult ViewVault(WalletId walletId, SigningContextModel signingContext)
{
return View(nameof(WalletSendVault), new WalletSendVaultModel()
{
SigningContext = signingContext,
WalletId = walletId.ToString(),
WebsocketPath = this.Url.Action(nameof(UIVaultController.VaultBridgeConnection), "UIVault", new { walletId = walletId.ToString() })
});
return View(nameof(WalletSendVault),
new WalletSendVaultModel()
{
SigningContext = signingContext,
WalletId = walletId.ToString(),
WebsocketPath = this.Url.Action(nameof(UIVaultController.VaultBridgeConnection), "UIVault",
new { walletId = walletId.ToString() })
});
}
[HttpPost]
[Route("{walletId}/vault")]
public IActionResult WalletSendVault([ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, WalletSendVaultModel model)
public IActionResult WalletSendVault([ModelBinder(typeof(WalletIdModelBinder))] WalletId walletId,
WalletSendVaultModel model)
{
return RedirectToWalletPSBTReady(new WalletPSBTReadyViewModel()
{
SigningContext = model.SigningContext
});
return RedirectToWalletPSBTReady(new WalletPSBTReadyViewModel() { SigningContext = model.SigningContext });
}
private IActionResult RedirectToWalletPSBTReady(WalletPSBTReadyViewModel vm)
@ -886,7 +980,8 @@ namespace BTCPayServer.Controllers
redirectVm.FormParameters.Add("SigningContext.PSBT", signingContext.PSBT);
redirectVm.FormParameters.Add("SigningContext.OriginalPSBT", signingContext.OriginalPSBT);
redirectVm.FormParameters.Add("SigningContext.PayJoinBIP21", signingContext.PayJoinBIP21);
redirectVm.FormParameters.Add("SigningContext.EnforceLowR", signingContext.EnforceLowR?.ToString(CultureInfo.InvariantCulture));
redirectVm.FormParameters.Add("SigningContext.EnforceLowR",
signingContext.EnforceLowR?.ToString(CultureInfo.InvariantCulture));
redirectVm.FormParameters.Add("SigningContext.ChangeAddress", signingContext.ChangeAddress);
}
@ -897,29 +992,21 @@ namespace BTCPayServer.Controllers
AspController = "UIWallets",
AspAction = nameof(WalletPSBT),
RouteParameters = { { "walletId", this.RouteData?.Values["walletId"]?.ToString() } },
FormParameters =
{
{ "psbt", vm.PSBT },
{ "fileName", vm.FileName },
{ "command", "decode" },
}
FormParameters = { { "psbt", vm.PSBT }, { "fileName", vm.FileName }, { "command", "decode" }, }
};
return View("PostRedirect", redirectVm);
}
[HttpGet("{walletId}/psbt/seed")]
public IActionResult SignWithSeed([ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, SigningContextModel signingContext)
public IActionResult SignWithSeed([ModelBinder(typeof(WalletIdModelBinder))] WalletId walletId,
SigningContextModel signingContext)
{
return View(nameof(SignWithSeed), new SignWithSeedViewModel
{
SigningContext = signingContext
});
return View(nameof(SignWithSeed), new SignWithSeedViewModel { SigningContext = signingContext });
}
[HttpPost("{walletId}/psbt/seed")]
public async Task<IActionResult> SignWithSeed([ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, SignWithSeedViewModel viewModel)
public async Task<IActionResult> SignWithSeed([ModelBinder(typeof(WalletIdModelBinder))] WalletId walletId,
SignWithSeedViewModel viewModel)
{
if (!ModelState.IsValid)
{
@ -957,7 +1044,8 @@ namespace BTCPayServer.Controllers
RootedKeyPath rootedKeyPath = signingKeySettings.GetRootedKeyPath();
if (rootedKeyPath == null)
{
ModelState.AddModelError(nameof(viewModel.SeedOrKey), "The master fingerprint and/or account key path of your seed are not set in the wallet settings.");
ModelState.AddModelError(nameof(viewModel.SeedOrKey),
"The master fingerprint and/or account key path of your seed are not set in the wallet settings.");
return View(nameof(SignWithSeed), viewModel);
}
// The user gave the root key, let's try to rebase the PSBT, and derive the account private key
@ -968,7 +1056,8 @@ namespace BTCPayServer.Controllers
}
else
{
ModelState.AddModelError(nameof(viewModel.SeedOrKey), "The master fingerprint does not match the one set in your wallet settings. Probable causes are: wrong seed, wrong passphrase or wrong fingerprint in your wallet settings.");
ModelState.AddModelError(nameof(viewModel.SeedOrKey),
"The master fingerprint does not match the one set in your wallet settings. Probable causes are: wrong seed, wrong passphrase or wrong fingerprint in your wallet settings.");
return View(nameof(SignWithSeed), viewModel);
}
@ -979,17 +1068,15 @@ namespace BTCPayServer.Controllers
var changed = psbt.PSBTChanged(() => psbt.SignAll(settings.AccountDerivation, signingKey, rootedKeyPath));
if (!changed)
{
var update = new UpdatePSBTRequest()
{
PSBT = psbt,
DerivationScheme = settings.AccountDerivation
};
var update = new UpdatePSBTRequest() { PSBT = psbt, DerivationScheme = settings.AccountDerivation };
update.RebaseKeyPaths = settings.GetPSBTRebaseKeyRules().ToList();
psbt = (await ExplorerClientProvider.GetExplorerClient(network).UpdatePSBTAsync(update))?.PSBT;
changed = psbt is not null && psbt.PSBTChanged(() => psbt.SignAll(settings.AccountDerivation, signingKey, rootedKeyPath));
changed = psbt is not null && psbt.PSBTChanged(() =>
psbt.SignAll(settings.AccountDerivation, signingKey, rootedKeyPath));
if (!changed)
{
ModelState.AddModelError(nameof(viewModel.SeedOrKey), "Impossible to sign the transaction. Probable causes: Incorrect account key path in wallet settings or PSBT already signed.");
ModelState.AddModelError(nameof(viewModel.SeedOrKey),
"Impossible to sign the transaction. Probable causes: Incorrect account key path in wallet settings or PSBT already signed.");
return View(nameof(SignWithSeed), viewModel);
}
}
@ -1021,8 +1108,10 @@ namespace BTCPayServer.Controllers
var vm = new RescanWalletModel();
vm.IsFullySync = _dashboard.IsFullySynched(walletId.CryptoCode, out var unused);
vm.IsServerAdmin = (await _authorizationService.AuthorizeAsync(User, Policies.CanModifyServerSettings)).Succeeded;
vm.IsSupportedByCurrency = _dashboard.Get(walletId.CryptoCode)?.Status?.BitcoinStatus?.Capabilities?.CanScanTxoutSet == true;
vm.IsServerAdmin = (await _authorizationService.AuthorizeAsync(User, Policies.CanModifyServerSettings))
.Succeeded;
vm.IsSupportedByCurrency =
_dashboard.Get(walletId.CryptoCode)?.Status?.BitcoinStatus?.Capabilities?.CanScanTxoutSet == true;
var explorer = ExplorerClientProvider.GetExplorerClient(walletId.CryptoCode);
var scanProgress = await explorer.GetScanUTXOSetInformationAsync(paymentMethod.AccountDerivation);
if (scanProgress != null)
@ -1040,12 +1129,15 @@ namespace BTCPayServer.Controllers
vm.RemainingTime = TimeSpan.FromSeconds(scanProgress.Progress.RemainingSeconds).PrettyPrint();
}
}
if (scanProgress.Status == ScanUTXOStatus.Complete)
{
vm.LastSuccess = scanProgress.Progress;
vm.TimeOfScan = (scanProgress.Progress.CompletedAt.Value - scanProgress.Progress.StartedAt).PrettyPrint();
vm.TimeOfScan = (scanProgress.Progress.CompletedAt.Value - scanProgress.Progress.StartedAt)
.PrettyPrint();
}
}
return View(vm);
}
@ -1063,12 +1155,13 @@ namespace BTCPayServer.Controllers
var explorer = ExplorerClientProvider.GetExplorerClient(walletId.CryptoCode);
try
{
await explorer.ScanUTXOSetAsync(paymentMethod.AccountDerivation, vm.BatchSize, vm.GapLimit, vm.StartingIndex);
await explorer.ScanUTXOSetAsync(paymentMethod.AccountDerivation, vm.BatchSize, vm.GapLimit,
vm.StartingIndex);
}
catch (NBXplorerException ex) when (ex.Error.Code == "scanutxoset-in-progress")
{
}
return RedirectToAction();
}
@ -1077,7 +1170,8 @@ namespace BTCPayServer.Controllers
return GetCurrentStore().GetDerivationSchemeSettings(NetworkProvider, walletId.CryptoCode);
}
private static async Task<IMoney> GetBalanceAsMoney(BTCPayWallet wallet, DerivationStrategyBase derivationStrategy)
private static async Task<IMoney> GetBalanceAsMoney(BTCPayWallet wallet,
DerivationStrategyBase derivationStrategy)
{
using CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
try
@ -1117,60 +1211,68 @@ namespace BTCPayServer.Controllers
switch (command)
{
case "cpfp":
{
selectedTransactions ??= Array.Empty<string>();
if (selectedTransactions.Length == 0)
{
selectedTransactions ??= Array.Empty<string>();
if (selectedTransactions.Length == 0)
{
TempData[WellKnownTempData.ErrorMessage] = $"No transaction selected";
return RedirectToAction(nameof(WalletTransactions), new { walletId });
}
var parameters = new MultiValueDictionary<string, string>();
parameters.Add("walletId", walletId.ToString());
int i = 0;
foreach (var tx in selectedTransactions)
{
parameters.Add($"transactionHashes[{i}]", tx);
i++;
}
parameters.Add("returnUrl", Url.Action(nameof(WalletTransactions), new { walletId }));
return View("PostRedirect", new PostRedirectViewModel
TempData[WellKnownTempData.ErrorMessage] = $"No transaction selected";
return RedirectToAction(nameof(WalletTransactions), new { walletId });
}
var parameters = new MultiValueDictionary<string, string>();
parameters.Add("walletId", walletId.ToString());
int i = 0;
foreach (var tx in selectedTransactions)
{
parameters.Add($"transactionHashes[{i}]", tx);
i++;
}
parameters.Add("returnUrl", Url.Action(nameof(WalletTransactions), new { walletId }));
return View("PostRedirect",
new PostRedirectViewModel
{
AspController = "UIWallets",
AspAction = nameof(UIWalletsController.WalletCPFP),
RouteParameters = { { "walletId", walletId.ToString() } },
FormParameters = parameters
});
}
}
case "prune":
{
var result = await ExplorerClientProvider.GetExplorerClient(walletId.CryptoCode)
.PruneAsync(derivationScheme.AccountDerivation, new PruneRequest(), cancellationToken);
if (result.TotalPruned == 0)
{
var result = await ExplorerClientProvider.GetExplorerClient(walletId.CryptoCode).PruneAsync(derivationScheme.AccountDerivation, new PruneRequest(), cancellationToken);
if (result.TotalPruned == 0)
{
TempData[WellKnownTempData.SuccessMessage] = "The wallet is already pruned";
}
else
{
TempData[WellKnownTempData.SuccessMessage] =
$"The wallet has been successfully pruned ({result.TotalPruned} transactions have been removed from the history)";
}
TempData[WellKnownTempData.SuccessMessage] = "The wallet is already pruned";
}
else
{
TempData[WellKnownTempData.SuccessMessage] =
$"The wallet has been successfully pruned ({result.TotalPruned} transactions have been removed from the history)";
}
return RedirectToAction(nameof(WalletTransactions), new { walletId });
}
return RedirectToAction(nameof(WalletTransactions), new { walletId });
}
case "clear" when User.IsInRole(Roles.ServerAdmin):
{
if (Version.TryParse(_dashboard.Get(walletId.CryptoCode)?.Status?.Version ?? "0.0.0.0",
out var v) &&
v < new Version(2, 2, 4))
{
if (Version.TryParse(_dashboard.Get(walletId.CryptoCode)?.Status?.Version ?? "0.0.0.0", out var v) &&
v < new Version(2, 2, 4))
{
TempData[WellKnownTempData.ErrorMessage] = "This version of NBXplorer doesn't support this operation, please upgrade to 2.2.4 or above";
}
else
{
await ExplorerClientProvider.GetExplorerClient(walletId.CryptoCode)
.WipeAsync(derivationScheme.AccountDerivation, cancellationToken);
TempData[WellKnownTempData.SuccessMessage] = "The transactions have been wiped out, to restore your balance, rescan the wallet.";
}
return RedirectToAction(nameof(WalletTransactions), new { walletId });
TempData[WellKnownTempData.ErrorMessage] =
"This version of NBXplorer doesn't support this operation, please upgrade to 2.2.4 or above";
}
else
{
await ExplorerClientProvider.GetExplorerClient(walletId.CryptoCode)
.WipeAsync(derivationScheme.AccountDerivation, cancellationToken);
TempData[WellKnownTempData.SuccessMessage] =
"The transactions have been wiped out, to restore your balance, rescan the wallet.";
}
return RedirectToAction(nameof(WalletTransactions), new { walletId });
}
default:
return NotFound();
}
@ -1199,7 +1301,6 @@ namespace BTCPayServer.Controllers
public class SendToAddressResult
{
[JsonProperty("psbt")]
public string PSBT { get; set; }
[JsonProperty("psbt")] public string PSBT { get; set; }
}
}

View File

@ -1,3 +1,4 @@
using System.Linq;
using NBXplorer;
using Newtonsoft.Json.Linq;
@ -7,20 +8,30 @@ namespace BTCPayServer.Data
{
public static APIKeyBlob GetBlob(this APIKeyData apiKeyData)
{
var result = apiKeyData.Blob == null
? new APIKeyBlob()
: JObject.Parse(ZipUtils.Unzip(apiKeyData.Blob)).ToObject<APIKeyBlob>();
return result;
return GetBlob<APIKeyBlob>(apiKeyData.Blob);
}
public static bool SetBlob(this APIKeyData apiKeyData, APIKeyBlob blob)
{
var original = new Serializer(null).ToString(apiKeyData.GetBlob());
var newBlob = new Serializer(null).ToString(blob);
if (original == newBlob)
var newBlob = SerializeBlob(blob);
if (apiKeyData?.Blob?.SequenceEqual(newBlob) is true)
return false;
apiKeyData.Blob = ZipUtils.Zip(newBlob);
apiKeyData.Blob = newBlob;
return true;
}
public static T GetBlob<T>(this byte[] data)
{
var result = data == null
? default
: JObject.Parse(ZipUtils.Unzip(data)).ToObject<T>();
return result;
}
public static byte[] SerializeBlob<T>(this T blob)
{
var newBlob = new Serializer(null).ToString(blob);
return ZipUtils.Zip(newBlob);
}
}
}

View File

@ -4,9 +4,10 @@ namespace BTCPayServer.Data
{
public static class InvoiceDataExtensions
{
public static InvoiceEntity GetBlob(this Data.InvoiceData invoiceData, BTCPayNetworkProvider networks)
public static InvoiceEntity GetBlob(this InvoiceData invoiceData, BTCPayNetworkProvider networks)
{
var entity = NBitcoin.JsonConverters.Serializer.ToObject<InvoiceEntity>(ZipUtils.Unzip(invoiceData.Blob), null);
var entity = InvoiceRepository.FromBytes<InvoiceEntity>(invoiceData.Blob);
entity.Networks = networks;
if (entity.Metadata is null)
{

View File

@ -6,7 +6,7 @@ namespace BTCPayServer.Data
{
public static class PaymentDataExtensions
{
public static PaymentEntity GetBlob(this Data.PaymentData paymentData, BTCPayNetworkProvider networks)
public static PaymentEntity GetBlob(this PaymentData paymentData, BTCPayNetworkProvider networks)
{
var unziped = ZipUtils.Unzip(paymentData.Blob);
var cryptoCode = "BTC";

View File

@ -5,6 +5,7 @@ using System.Text;
using System.Threading.Tasks;
using BTCPayServer;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.Events;
@ -243,8 +244,8 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
&& data.State == PayoutState.AwaitingPayment)
.ToListAsync();
var pullPaymentIds = payouts.Select(data => data.PullPaymentDataId).Distinct().ToArray();
var storeId = payouts.First().PullPaymentData.StoreId;
var pullPaymentIds = payouts.Select(data => data.PullPaymentDataId).Distinct().Where(s => s!= null).ToArray();
var storeId = payouts.First().StoreDataId;
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(paymentMethodId.CryptoCode);
List<string> bip21 = new List<string>();
foreach (var payout in payouts)
@ -261,10 +262,14 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
{
case UriClaimDestination uriClaimDestination:
uriClaimDestination.BitcoinUrl.Amount = new Money(blob.CryptoAmount.Value, MoneyUnit.BTC);
bip21.Add(uriClaimDestination.ToString());
var newUri = new UriBuilder(uriClaimDestination.BitcoinUrl.Uri);
BTCPayServerClient.AppendPayloadToQuery(newUri, new KeyValuePair<string, object>("payout", payout.Id));
bip21.Add(newUri.Uri.ToString());
break;
case AddressClaimDestination addressClaimDestination:
bip21.Add(network.GenerateBIP21(addressClaimDestination.Address.ToString(), new Money(blob.CryptoAmount.Value, MoneyUnit.BTC)).ToString());
var bip21New = network.GenerateBIP21(addressClaimDestination.Address.ToString(), new Money(blob.CryptoAmount.Value, MoneyUnit.BTC));
bip21New.QueryParams.Add("payout", payout.Id);
bip21.Add(bip21New.ToString());
break;
}
}
@ -326,7 +331,7 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
}
}
if (proof.TransactionId is null && !proof.Candidates.Contains(proof.TransactionId))
if (proof.TransactionId is not null && !proof.Candidates.Contains(proof.TransactionId))
{
proof.TransactionId = null;
}
@ -366,8 +371,8 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
await using var ctx = _dbContextFactory.CreateContext();
var payouts = await ctx.Payouts
.Include(o => o.StoreData)
.Include(o => o.PullPaymentData)
.ThenInclude(o => o.StoreData)
.Where(p => p.State == PayoutState.AwaitingPayment)
.Where(p => p.PaymentMethodId == paymentMethodId.ToString())
#pragma warning disable CA1307 // Specify StringComparison
@ -386,7 +391,7 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
BTCPayServer.Extensions.RoundUp(payoutBlob.CryptoAmount.Value, network.Divisibility))
return;
var derivationSchemeSettings = payout.PullPaymentData.StoreData
var derivationSchemeSettings = payout.StoreData
.GetDerivationSchemeSettings(_btcPayNetworkProvider, newTransaction.CryptoCode).AccountDerivation;
var storeWalletMatched = (await _explorerClientProvider.GetExplorerClient(newTransaction.CryptoCode)
@ -403,19 +408,19 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
if (isInternal)
{
payout.State = PayoutState.InProgress;
var walletId = new WalletId(payout.PullPaymentData.StoreId, newTransaction.CryptoCode);
var walletId = new WalletId(payout.StoreDataId, newTransaction.CryptoCode);
_eventAggregator.Publish(new UpdateTransactionLabel(walletId,
newTransaction.NewTransactionEvent.TransactionData.TransactionHash,
UpdateTransactionLabel.PayoutTemplate(payout.Id, payout.PullPaymentDataId, walletId.ToString())));
}
else
{
await _notificationSender.SendNotification(new StoreScope(payout.PullPaymentData.StoreId),
await _notificationSender.SendNotification(new StoreScope(payout.StoreDataId),
new ExternalPayoutTransactionNotification()
{
PaymentMethod = payout.PaymentMethodId,
PayoutId = payout.Id,
StoreId = payout.PullPaymentData.StoreId
StoreId = payout.StoreDataId
});
}
@ -431,7 +436,7 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
}
}
private void SetProofBlob(PayoutData data, PayoutTransactionOnChainBlob blob)
public 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

View File

@ -1,3 +1,4 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
@ -15,8 +16,8 @@ public interface IPayoutHandler
public Task TrackClaim(PaymentMethodId paymentMethodId, IClaimDestination claimDestination);
//Allows payout handler to parse payout destinations on its own
public Task<(IClaimDestination destination, string error)> ParseClaimDestination(PaymentMethodId paymentMethodId, string destination);
public (bool valid, string error) ValidateClaimDestination(IClaimDestination claimDestination, PullPaymentBlob pullPaymentBlob);
public async Task<(IClaimDestination destination, string error)> ParseAndValidateClaimDestination(PaymentMethodId paymentMethodId, string destination, PullPaymentBlob pullPaymentBlob)
public (bool valid, string? error) ValidateClaimDestination(IClaimDestination claimDestination, PullPaymentBlob? pullPaymentBlob);
public async Task<(IClaimDestination? destination, string? error)> ParseAndValidateClaimDestination(PaymentMethodId paymentMethodId, string destination, PullPaymentBlob? pullPaymentBlob)
{
var res = await ParseClaimDestination(paymentMethodId, destination);
if (res.destination is null)

View File

@ -108,7 +108,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike
if (claimDestination is not BoltInvoiceClaimDestination bolt)
return (true, null);
var invoice = bolt.PaymentRequest;
if ((invoice.ExpiryDate.UtcDateTime - DateTime.UtcNow) < pullPaymentBlob.BOLT11Expiration)
if (pullPaymentBlob is not null && (invoice.ExpiryDate.UtcDateTime - DateTime.UtcNow) < pullPaymentBlob.BOLT11Expiration)
{
return (false,
$"The BOLT11 invoice must have an expiry date of at least {(long)pullPaymentBlob.BOLT11Expiration.TotalDays} days from submission (Provided was only {(invoice.ExpiryDate.UtcDateTime - DateTime.UtcNow).Days}).");

View File

@ -19,9 +19,9 @@ namespace BTCPayServer.Data
if (includePullPayment)
query = query.Include(p => p.PullPaymentData);
if (includeStore)
query = query.Include(p => p.PullPaymentData.StoreData);
query = query.Include(p => p.StoreData);
var payout = await query.Where(p => p.Id == payoutId &&
p.PullPaymentData.StoreId == storeId).FirstOrDefaultAsync();
p.StoreDataId == storeId).FirstOrDefaultAsync();
if (payout is null)
return null;
return payout;

View File

@ -30,6 +30,8 @@ namespace BTCPayServer.Data
[JsonProperty(ItemConverterType = typeof(PaymentMethodIdJsonConverter))]
public PaymentMethodId[] SupportedPaymentMethods { get; set; }
public bool AutoApproveClaims { get; set; }
public class PullPaymentView
{
public string Title { get; set; }

View File

@ -2,6 +2,7 @@ namespace BTCPayServer.Events
{
public class SettingsChanged<T>
{
public string SettingsName { get; set; }
public T Settings { get; set; }
public override string ToString()
{

View File

@ -1,5 +1,6 @@
using BTCPayServer;
using BTCPayServer.Client.Models;
using BTCPayServer.Controllers;
using BTCPayServer.Services.Apps;
using Microsoft.AspNetCore.Http;
@ -32,7 +33,7 @@ namespace Microsoft.AspNetCore.Mvc
return urlHelper.GetUriByAction(
action: nameof(UIPaymentRequestController.ViewPaymentRequest),
controller: "UIPaymentRequest",
values: new { id = paymentRequestId },
values: new { payReqId = paymentRequestId },
scheme, host, pathbase);
}
@ -50,7 +51,7 @@ namespace Microsoft.AspNetCore.Mvc
return urlHelper.GetUriByAction(
action: nameof(UIInvoiceController.Invoice),
controller: "UIInvoice",
values: new { invoiceId = invoiceId },
values: new { invoiceId },
scheme, host, pathbase);
}
@ -59,17 +60,17 @@ namespace Microsoft.AspNetCore.Mvc
return urlHelper.GetUriByAction(
action: nameof(UIInvoiceController.Checkout),
controller: "UIInvoice",
values: new { invoiceId = invoiceId },
values: new { invoiceId },
scheme, host, pathbase);
}
public static string PayoutLink(this LinkGenerator urlHelper, string walletIdOrStoreId, string pullPaymentId, string scheme, HostString host, string pathbase)
public static string PayoutLink(this LinkGenerator urlHelper, string walletIdOrStoreId, string pullPaymentId, PayoutState payoutState,string scheme, HostString host, string pathbase)
{
WalletId.TryParse(walletIdOrStoreId, out var wallet);
return urlHelper.GetUriByAction(
action: nameof(UIStorePullPaymentsController.Payouts),
controller: "UIStorePullPayments",
values: new { storeId = wallet?.StoreId ?? walletIdOrStoreId, pullPaymentId },
values: new { storeId = wallet?.StoreId ?? walletIdOrStoreId, pullPaymentId, payoutState },
scheme, host, pathbase);
}
}

View File

@ -13,11 +13,17 @@ namespace BTCPayServer.HostedServices
private CancellationTokenSource _Cts = new CancellationTokenSource();
protected Task[] _Tasks;
public readonly Logs Logs;
public BaseAsyncService(Logs logs)
protected BaseAsyncService(Logs logs)
{
Logs = logs;
}
protected BaseAsyncService(ILogger logger)
{
Logs = new Logs() { PayServer = logger, Events = logger, Configuration = logger};
}
public virtual Task StartAsync(CancellationToken cancellationToken)
{
_Tasks = InitializeTasks();

View File

@ -8,6 +8,7 @@ using BTCPayServer.Data;
using BTCPayServer.Logging;
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@ -58,7 +59,7 @@ namespace BTCPayServer.HostedServices
}
var invoiceQuery = new InvoiceQuery { IncludeArchived = true };
var totalCount = await _invoiceRepository.GetInvoicesTotal(invoiceQuery);
var totalCount = await CountInvoices();
const int PAGE_SIZE = 1000;
var totalPages = Math.Ceiling(totalCount * 1.0m / PAGE_SIZE);
Logs.PayServer.LogInformation($"Importing {totalCount} invoices into the search table in {totalPages - startFromPage} pages");
@ -118,5 +119,11 @@ namespace BTCPayServer.HostedServices
}
Logs.PayServer.LogInformation($"Full invoice search import successful");
}
private Task<int> CountInvoices()
{
using var ctx = _dbContextFactory.CreateContext();
return ctx.Invoices.CountAsync();
}
}
}

View File

@ -25,6 +25,12 @@ namespace BTCPayServer.HostedServices
_EventAggregator = eventAggregator;
Logs = logs;
}
public EventHostedServiceBase(EventAggregator eventAggregator, ILogger logger)
{
_EventAggregator = eventAggregator;
Logs = new Logs() { PayServer = logger, Events = logger, Configuration = logger};
}
readonly Channel<object> _Events = Channel.CreateUnbounded<object>();
public async Task ProcessEvents(CancellationToken cancellationToken)

View File

@ -35,8 +35,10 @@ namespace BTCPayServer.HostedServices
public string EmbeddedCSS { get; set; }
public PaymentMethodId[] PaymentMethodIds { get; set; }
public TimeSpan? Period { get; set; }
public bool AutoApproveClaims { get; set; }
public TimeSpan? BOLT11Expiration { get; set; }
}
public class PullPaymentHostedService : BaseAsyncService
{
public class CancelRequest
@ -46,15 +48,18 @@ namespace BTCPayServer.HostedServices
ArgumentNullException.ThrowIfNull(pullPaymentId);
PullPaymentId = pullPaymentId;
}
public CancelRequest(string[] payoutIds)
{
ArgumentNullException.ThrowIfNull(payoutIds);
PayoutIds = payoutIds;
}
public string PullPaymentId { get; set; }
public string[] PayoutIds { get; set; }
internal TaskCompletionSource<bool> Completion { get; set; }
}
public class PayoutApproval
{
public enum Result
@ -65,6 +70,7 @@ namespace BTCPayServer.HostedServices
TooLowAmount,
OldRevision
}
public string PayoutId { get; set; }
public int Revision { get; set; }
public decimal Rate { get; set; }
@ -89,6 +95,7 @@ namespace BTCPayServer.HostedServices
}
}
}
public async Task<string> CreatePullPayment(CreatePullPayment create)
{
ArgumentNullException.ThrowIfNull(create);
@ -96,7 +103,9 @@ namespace BTCPayServer.HostedServices
throw new ArgumentException("Amount out of bound", nameof(create));
using var ctx = this._dbContextFactory.CreateContext();
var o = new Data.PullPaymentData();
o.StartDate = create.StartsAt is DateTimeOffset date ? date : DateTimeOffset.UtcNow - TimeSpan.FromSeconds(1.0);
o.StartDate = create.StartsAt is DateTimeOffset date
? date
: DateTimeOffset.UtcNow - TimeSpan.FromSeconds(1.0);
o.EndDate = create.ExpiresAt is DateTimeOffset date2 ? new DateTimeOffset?(date2) : null;
o.Period = create.Period is TimeSpan period ? (long?)period.TotalSeconds : null;
o.Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(20));
@ -109,6 +118,7 @@ namespace BTCPayServer.HostedServices
Limit = create.Amount,
Period = o.Period is long periodSeconds ? (TimeSpan?)TimeSpan.FromSeconds(periodSeconds) : null,
SupportedPaymentMethods = create.PaymentMethodIds,
AutoApproveClaims = create.AutoApproveClaims,
View = new PullPaymentBlob.PullPaymentView()
{
Title = create.Name ?? string.Empty,
@ -136,19 +146,21 @@ namespace BTCPayServer.HostedServices
class PayoutRequest
{
public PayoutRequest(TaskCompletionSource<ClaimRequest.ClaimResponse> completionSource, ClaimRequest request)
public PayoutRequest(TaskCompletionSource<ClaimRequest.ClaimResponse> completionSource,
ClaimRequest request)
{
ArgumentNullException.ThrowIfNull(request);
ArgumentNullException.ThrowIfNull(completionSource);
Completion = completionSource;
ClaimRequest = request;
}
public TaskCompletionSource<ClaimRequest.ClaimResponse> Completion { get; set; }
public ClaimRequest ClaimRequest { get; }
}
public PullPaymentHostedService(ApplicationDbContextFactory dbContextFactory,
BTCPayNetworkJsonSerializerSettings jsonSerializerSettings,
CurrencyNameTable currencyNameTable,
EventAggregator eventAggregator,
BTCPayNetworkProvider networkProvider,
NotificationSender notificationSender,
@ -159,7 +171,6 @@ namespace BTCPayServer.HostedServices
{
_dbContextFactory = dbContextFactory;
_jsonSerializerSettings = jsonSerializerSettings;
_currencyNameTable = currencyNameTable;
_eventAggregator = eventAggregator;
_networkProvider = networkProvider;
_notificationSender = notificationSender;
@ -171,7 +182,6 @@ namespace BTCPayServer.HostedServices
Channel<object> _Channel;
private readonly ApplicationDbContextFactory _dbContextFactory;
private readonly BTCPayNetworkJsonSerializerSettings _jsonSerializerSettings;
private readonly CurrencyNameTable _currencyNameTable;
private readonly EventAggregator _eventAggregator;
private readonly BTCPayNetworkProvider _networkProvider;
private readonly NotificationSender _notificationSender;
@ -187,6 +197,7 @@ namespace BTCPayServer.HostedServices
{
payoutHandler.StartBackgroundCheck(Subscribe);
}
return new[] { Loop() };
}
@ -211,14 +222,17 @@ namespace BTCPayServer.HostedServices
{
await HandleApproval(approv);
}
if (o is CancelRequest cancel)
{
await HandleCancel(cancel);
}
if (o is InternalPayoutPaidRequest paid)
{
await HandleMarkPaid(paid);
}
foreach (IPayoutHandler payoutHandler in _payoutHandlers)
{
try
@ -235,14 +249,16 @@ namespace BTCPayServer.HostedServices
public Task<RateResult> GetRate(PayoutData payout, string explicitRateRule, CancellationToken cancellationToken)
{
var ppBlob = payout.PullPaymentData.GetBlob();
var currencyPair = new Rating.CurrencyPair(payout.GetPaymentMethodId().CryptoCode, ppBlob.Currency);
var ppBlob = payout.PullPaymentData?.GetBlob();
var payoutPaymentMethod = payout.GetPaymentMethodId();
var currencyPair = new Rating.CurrencyPair(payoutPaymentMethod.CryptoCode,
ppBlob?.Currency ?? payoutPaymentMethod.CryptoCode);
Rating.RateRule rule = null;
try
{
if (explicitRateRule is null)
{
var storeBlob = payout.PullPaymentData.StoreData.GetStoreBlob();
var storeBlob = payout.StoreData.GetStoreBlob();
var rules = storeBlob.GetRateRules(_networkProvider);
rules.Spread = 0.0m;
rule = rules.GetRuleFor(currencyPair);
@ -256,60 +272,73 @@ namespace BTCPayServer.HostedServices
{
throw new FormatException("Invalid RateRule");
}
return _rateFetcher.FetchRate(rule, cancellationToken);
}
public Task<PayoutApproval.Result> Approve(PayoutApproval approval)
{
approval.Completion = new TaskCompletionSource<PayoutApproval.Result>(TaskCreationOptions.RunContinuationsAsynchronously);
approval.Completion =
new TaskCompletionSource<PayoutApproval.Result>(TaskCreationOptions.RunContinuationsAsynchronously);
if (!_Channel.Writer.TryWrite(approval))
throw new ObjectDisposedException(nameof(PullPaymentHostedService));
return approval.Completion.Task;
}
private async Task HandleApproval(PayoutApproval req)
{
try
{
using var ctx = _dbContextFactory.CreateContext();
var payout = await ctx.Payouts.Include(p => p.PullPaymentData).Where(p => p.Id == req.PayoutId).FirstOrDefaultAsync();
var payout = await ctx.Payouts.Include(p => p.PullPaymentData).Where(p => p.Id == req.PayoutId)
.FirstOrDefaultAsync();
if (payout is null)
{
req.Completion.SetResult(PayoutApproval.Result.NotFound);
return;
}
if (payout.State != PayoutState.AwaitingApproval)
{
req.Completion.SetResult(PayoutApproval.Result.InvalidState);
return;
}
var payoutBlob = payout.GetBlob(this._jsonSerializerSettings);
if (payoutBlob.Revision != req.Revision)
{
req.Completion.SetResult(PayoutApproval.Result.OldRevision);
return;
}
if (!PaymentMethodId.TryParse(payout.PaymentMethodId, out var paymentMethod))
{
req.Completion.SetResult(PayoutApproval.Result.NotFound);
return;
}
payout.State = PayoutState.AwaitingPayment;
if (paymentMethod.CryptoCode == payout.PullPaymentData.GetBlob().Currency)
if (payout.PullPaymentData is null || paymentMethod.CryptoCode == payout.PullPaymentData.GetBlob().Currency)
req.Rate = 1.0m;
var cryptoAmount = payoutBlob.Amount / req.Rate;
var payoutHandler = _payoutHandlers.FindPayoutHandler(paymentMethod);
if (payoutHandler is null)
throw new InvalidOperationException($"No payout handler for {paymentMethod}");
var dest = await payoutHandler.ParseClaimDestination(paymentMethod, payoutBlob.Destination);
decimal minimumCryptoAmount = await payoutHandler.GetMinimumPayoutAmount(paymentMethod, dest.destination);
decimal minimumCryptoAmount =
await payoutHandler.GetMinimumPayoutAmount(paymentMethod, dest.destination);
if (cryptoAmount < minimumCryptoAmount)
{
req.Completion.TrySetResult(PayoutApproval.Result.TooLowAmount);
return;
}
payoutBlob.CryptoAmount = BTCPayServer.Extensions.RoundUp(cryptoAmount, _networkProvider.GetNetwork(paymentMethod.CryptoCode).Divisibility);
payoutBlob.CryptoAmount = Extensions.RoundUp(cryptoAmount,
_networkProvider.GetNetwork(paymentMethod.CryptoCode).Divisibility);
payout.SetBlob(payoutBlob, _jsonSerializerSettings);
await ctx.SaveChangesAsync();
req.Completion.SetResult(PayoutApproval.Result.Ok);
}
catch (Exception ex)
@ -317,26 +346,31 @@ namespace BTCPayServer.HostedServices
req.Completion.TrySetException(ex);
}
}
private async Task HandleMarkPaid(InternalPayoutPaidRequest req)
{
try
{
await using var ctx = _dbContextFactory.CreateContext();
var payout = await ctx.Payouts.Include(p => p.PullPaymentData).Where(p => p.Id == req.Request.PayoutId).FirstOrDefaultAsync();
var payout = await ctx.Payouts.Include(p => p.PullPaymentData).Where(p => p.Id == req.Request.PayoutId)
.FirstOrDefaultAsync();
if (payout is null)
{
req.Completion.SetResult(PayoutPaidRequest.PayoutPaidResult.NotFound);
return;
}
if (payout.State != PayoutState.AwaitingPayment)
{
req.Completion.SetResult(PayoutPaidRequest.PayoutPaidResult.InvalidState);
return;
}
if (req.Request.Proof != null)
{
payout.SetProofBlob(req.Request.Proof);
}
payout.State = PayoutState.Completed;
await ctx.SaveChangesAsync();
req.Completion.SetResult(PayoutPaidRequest.PayoutPaidResult.Ok);
@ -353,40 +387,59 @@ namespace BTCPayServer.HostedServices
{
DateTimeOffset now = DateTimeOffset.UtcNow;
await using var ctx = _dbContextFactory.CreateContext();
var pp = await ctx.PullPayments.FindAsync(req.ClaimRequest.PullPaymentId);
var withoutPullPayment = req.ClaimRequest.PullPaymentId is null;
var pp = string.IsNullOrEmpty(req.ClaimRequest.PullPaymentId)
? null
: await ctx.PullPayments.FindAsync(req.ClaimRequest.PullPaymentId);
if (pp is null || pp.Archived)
if (!withoutPullPayment && (pp is null || pp.Archived))
{
req.Completion.TrySetResult(new ClaimRequest.ClaimResponse(ClaimRequest.ClaimResult.Archived));
return;
}
if (pp.IsExpired(now))
PullPaymentBlob ppBlob = null;
if (!withoutPullPayment)
{
req.Completion.TrySetResult(new ClaimRequest.ClaimResponse(ClaimRequest.ClaimResult.Expired));
return;
if (pp.IsExpired(now))
{
req.Completion.TrySetResult(new ClaimRequest.ClaimResponse(ClaimRequest.ClaimResult.Expired));
return;
}
if (!pp.HasStarted(now))
{
req.Completion.TrySetResult(
new ClaimRequest.ClaimResponse(ClaimRequest.ClaimResult.NotStarted));
return;
}
ppBlob = pp.GetBlob();
if (!ppBlob.SupportedPaymentMethods.Contains(req.ClaimRequest.PaymentMethodId))
{
req.Completion.TrySetResult(
new ClaimRequest.ClaimResponse(ClaimRequest.ClaimResult.PaymentMethodNotSupported));
return;
}
}
if (!pp.HasStarted(now))
{
req.Completion.TrySetResult(new ClaimRequest.ClaimResponse(ClaimRequest.ClaimResult.NotStarted));
return;
}
var ppBlob = pp.GetBlob();
var payoutHandler =
_payoutHandlers.FindPayoutHandler(req.ClaimRequest.PaymentMethodId);
if (!ppBlob.SupportedPaymentMethods.Contains(req.ClaimRequest.PaymentMethodId) || payoutHandler is null)
if (payoutHandler is null)
{
req.Completion.TrySetResult(new ClaimRequest.ClaimResponse(ClaimRequest.ClaimResult.PaymentMethodNotSupported));
req.Completion.TrySetResult(
new ClaimRequest.ClaimResponse(ClaimRequest.ClaimResult.PaymentMethodNotSupported));
return;
}
if (req.ClaimRequest.Destination.Id != null)
{
if (await ctx.Payouts.AnyAsync(data =>
data.Destination.Equals(req.ClaimRequest.Destination.Id) &&
data.State != PayoutState.Completed && data.State != PayoutState.Cancelled
data.Destination.Equals(req.ClaimRequest.Destination.Id) &&
data.State != PayoutState.Completed && data.State != PayoutState.Cancelled
))
{
req.Completion.TrySetResult(new ClaimRequest.ClaimResponse(ClaimRequest.ClaimResult.Duplicate));
return;
}
@ -400,22 +453,27 @@ namespace BTCPayServer.HostedServices
return;
}
var payouts = (await ctx.Payouts.GetPayoutInPeriod(pp, now)
.Where(p => p.State != PayoutState.Cancelled)
.ToListAsync())
.Select(o => new
{
Entity = o,
Blob = o.GetBlob(_jsonSerializerSettings)
});
var limit = ppBlob.Limit;
var totalPayout = payouts.Select(p => p.Blob.Amount).Sum();
var claimed = req.ClaimRequest.Value is decimal v ? v : limit - totalPayout;
if (totalPayout + claimed > limit)
var payoutsRaw = withoutPullPayment
? null
: await ctx.Payouts.GetPayoutInPeriod(pp, now)
.Where(p => p.State != PayoutState.Cancelled).ToListAsync();
var payouts = payoutsRaw?.Select(o => new { Entity = o, Blob = o.GetBlob(_jsonSerializerSettings) });
var limit = ppBlob?.Limit ?? 0;
var totalPayout = payouts?.Select(p => p.Blob.Amount)?.Sum();
var claimed = req.ClaimRequest.Value is decimal v ? v : limit - (totalPayout ?? 0);
if (totalPayout is not null && totalPayout + claimed > limit)
{
req.Completion.TrySetResult(new ClaimRequest.ClaimResponse(ClaimRequest.ClaimResult.Overdraft));
return;
}
if (!withoutPullPayment && (claimed < ppBlob.MinimumClaim || claimed == 0.0m))
{
req.Completion.TrySetResult(new ClaimRequest.ClaimResponse(ClaimRequest.ClaimResult.AmountTooLow));
return;
}
var payout = new PayoutData()
{
Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(20)),
@ -423,13 +481,9 @@ namespace BTCPayServer.HostedServices
State = PayoutState.AwaitingApproval,
PullPaymentDataId = req.ClaimRequest.PullPaymentId,
PaymentMethodId = req.ClaimRequest.PaymentMethodId.ToString(),
Destination = req.ClaimRequest.Destination.Id
Destination = req.ClaimRequest.Destination.Id,
StoreDataId = req.ClaimRequest.StoreId ?? pp?.StoreId
};
if (claimed < ppBlob.MinimumClaim || claimed == 0.0m)
{
req.Completion.TrySetResult(new ClaimRequest.ClaimResponse(ClaimRequest.ClaimResult.AmountTooLow));
return;
}
var payoutBlob = new PayoutBlob()
{
Amount = claimed,
@ -441,14 +495,34 @@ namespace BTCPayServer.HostedServices
{
await payoutHandler.TrackClaim(req.ClaimRequest.PaymentMethodId, req.ClaimRequest.Destination);
await ctx.SaveChangesAsync();
req.Completion.TrySetResult(new ClaimRequest.ClaimResponse(ClaimRequest.ClaimResult.Ok, payout));
await _notificationSender.SendNotification(new StoreScope(pp.StoreId), new PayoutNotification()
if (req.ClaimRequest.PreApprove.GetValueOrDefault(ppBlob?.AutoApproveClaims is true) )
{
StoreId = pp.StoreId,
Currency = ppBlob.Currency,
PaymentMethod = payout.PaymentMethodId,
PayoutId = pp.Id
});
payout.StoreData = await ctx.Stores.FindAsync(payout.StoreDataId);
var rateResult = await GetRate(payout, null, CancellationToken.None);
if (rateResult.BidAsk != null)
{
var approveResult = new TaskCompletionSource<PayoutApproval.Result>();
await HandleApproval(new PayoutApproval()
{
PayoutId = payout.Id, Revision = payoutBlob.Revision, Rate = rateResult.BidAsk.Ask, Completion =approveResult
});
if ((await approveResult.Task) == PayoutApproval.Result.Ok)
{
payout.State = PayoutState.AwaitingPayment;
}
}
}
req.Completion.TrySetResult(new ClaimRequest.ClaimResponse(ClaimRequest.ClaimResult.Ok, payout));
await _notificationSender.SendNotification(new StoreScope(payout.StoreDataId),
new PayoutNotification()
{
StoreId = payout.StoreDataId,
Currency = ppBlob?.Currency ?? req.ClaimRequest.PaymentMethodId.CryptoCode,
Status = payout.State,
PaymentMethod = payout.PaymentMethodId,
PayoutId = payout.Id
});
}
catch (DbUpdateException)
{
@ -460,6 +534,7 @@ namespace BTCPayServer.HostedServices
req.Completion.TrySetException(ex);
}
}
private async Task HandleCancel(CancelRequest cancel)
{
try
@ -471,15 +546,15 @@ namespace BTCPayServer.HostedServices
ctx.PullPayments.Attach(new Data.PullPaymentData() { Id = cancel.PullPaymentId, Archived = true })
.Property(o => o.Archived).IsModified = true;
payouts = await ctx.Payouts
.Where(p => p.PullPaymentDataId == cancel.PullPaymentId)
.ToListAsync();
.Where(p => p.PullPaymentDataId == cancel.PullPaymentId)
.ToListAsync();
}
else
{
var payoutIds = cancel.PayoutIds.ToHashSet();
payouts = await ctx.Payouts
.Where(p => payoutIds.Contains(p.Id))
.ToListAsync();
.Where(p => payoutIds.Contains(p.Id))
.ToListAsync();
}
foreach (var payout in payouts)
@ -487,6 +562,7 @@ namespace BTCPayServer.HostedServices
if (payout.State != PayoutState.Completed && payout.State != PayoutState.InProgress)
payout.State = PayoutState.Cancelled;
}
await ctx.SaveChangesAsync();
cancel.Completion.TrySetResult(true);
}
@ -495,6 +571,7 @@ namespace BTCPayServer.HostedServices
cancel.Completion.TrySetException(ex);
}
}
public Task Cancel(CancelRequest cancelRequest)
{
CancellationToken.ThrowIfCancellationRequested();
@ -508,7 +585,8 @@ namespace BTCPayServer.HostedServices
public Task<ClaimRequest.ClaimResponse> Claim(ClaimRequest request)
{
CancellationToken.ThrowIfCancellationRequested();
var cts = new TaskCompletionSource<ClaimRequest.ClaimResponse>(TaskCreationOptions.RunContinuationsAsynchronously);
var cts = new TaskCompletionSource<ClaimRequest.ClaimResponse>(TaskCreationOptions
.RunContinuationsAsynchronously);
if (!_Channel.Writer.TryWrite(new PayoutRequest(cts, request)))
throw new ObjectDisposedException(nameof(PullPaymentHostedService));
return cts.Task;
@ -524,7 +602,8 @@ namespace BTCPayServer.HostedServices
public Task<PayoutPaidRequest.PayoutPaidResult> MarkPaid(PayoutPaidRequest request)
{
CancellationToken.ThrowIfCancellationRequested();
var cts = new TaskCompletionSource<PayoutPaidRequest.PayoutPaidResult>(TaskCreationOptions.RunContinuationsAsynchronously);
var cts = new TaskCompletionSource<PayoutPaidRequest.PayoutPaidResult>(TaskCreationOptions
.RunContinuationsAsynchronously);
if (!_Channel.Writer.TryWrite(new InternalPayoutPaidRequest(cts, request)))
throw new ObjectDisposedException(nameof(PullPaymentHostedService));
return cts.Task;
@ -533,17 +612,18 @@ namespace BTCPayServer.HostedServices
class InternalPayoutPaidRequest
{
public InternalPayoutPaidRequest(TaskCompletionSource<PayoutPaidRequest.PayoutPaidResult> completionSource, PayoutPaidRequest request)
public InternalPayoutPaidRequest(TaskCompletionSource<PayoutPaidRequest.PayoutPaidResult> completionSource,
PayoutPaidRequest request)
{
ArgumentNullException.ThrowIfNull(request);
ArgumentNullException.ThrowIfNull(completionSource);
Completion = completionSource;
Request = request;
}
public TaskCompletionSource<PayoutPaidRequest.PayoutPaidResult> Completion { get; set; }
public PayoutPaidRequest Request { get; }
}
}
public class PayoutPaidRequest
@ -554,6 +634,7 @@ namespace BTCPayServer.HostedServices
NotFound,
InvalidState
}
public string PayoutId { get; set; }
public ManualPayoutProof Proof { get; set; }
@ -571,7 +652,6 @@ namespace BTCPayServer.HostedServices
throw new NotSupportedException();
}
}
}
public class ClaimRequest
@ -599,8 +679,10 @@ namespace BTCPayServer.HostedServices
default:
throw new NotSupportedException("Unsupported ClaimResult");
}
return null;
}
public class ClaimResponse
{
public ClaimResponse(ClaimResult result, PayoutData payoutData = null)
@ -608,9 +690,11 @@ namespace BTCPayServer.HostedServices
Result = result;
PayoutData = payoutData;
}
public ClaimResult Result { get; set; }
public PayoutData PayoutData { get; set; }
}
public enum ClaimResult
{
Ok,
@ -627,6 +711,7 @@ namespace BTCPayServer.HostedServices
public string PullPaymentId { get; set; }
public decimal? Value { get; set; }
public IClaimDestination Destination { get; set; }
public string StoreId { get; set; }
public bool? PreApprove { get; set; }
}
}

View File

@ -21,6 +21,7 @@ using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Payments.PayJoin;
using BTCPayServer.PayoutProcessors;
using BTCPayServer.Plugins;
using BTCPayServer.Security;
using BTCPayServer.Security.Bitpay;
@ -322,7 +323,8 @@ namespace BTCPayServer.Hosting
.ConfigurePrimaryHttpMessageHandler<Socks5HttpClientHandler>();
services.AddSingleton<IPayoutHandler, BitcoinLikePayoutHandler>();
services.AddSingleton<BitcoinLikePayoutHandler>();
services.AddSingleton<IPayoutHandler>(provider => provider.GetRequiredService<BitcoinLikePayoutHandler>());
services.AddSingleton<IPayoutHandler, LightningLikePayoutHandler>();
services.AddHttpClient(LightningLikePayoutHandler.LightningLikePayoutHandlerOnionNamedClient)
@ -402,6 +404,7 @@ namespace BTCPayServer.Hosting
services.AddScoped<BTCPayServerClient, LocalBTCPayServerClient>();
//also provide a factory that can impersonate user/store id
services.AddSingleton<IBTCPayServerClientFactory, BTCPayServerClientFactory>();
services.AddPayoutProcesors();
services.AddAPIKeyAuthentication();
services.AddBtcPayServerAuthenticationSchemes();

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@ -23,9 +24,14 @@ using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NBitcoin;
using NBitcoin.DataEncoders;
using NBXplorer;
using Newtonsoft.Json.Linq;
using PeterO.Cbor;
using PayoutData = BTCPayServer.Data.PayoutData;
using PullPaymentData = BTCPayServer.Data.PullPaymentData;
using StoreData = BTCPayServer.Data.StoreData;
namespace BTCPayServer.Hosting
{
@ -40,6 +46,8 @@ namespace BTCPayServer.Hosting
private readonly AppService _appService;
private readonly IEnumerable<IPayoutHandler> _payoutHandlers;
private readonly BTCPayNetworkJsonSerializerSettings _btcPayNetworkJsonSerializerSettings;
private readonly LightningAddressService _lightningAddressService;
private readonly ILogger<MigrationStartupTask> _logger;
private readonly UserManager<ApplicationUser> _userManager;
public IOptions<LightningNetworkOptions> LightningOptions { get; }
@ -54,9 +62,9 @@ namespace BTCPayServer.Hosting
AppService appService,
IEnumerable<IPayoutHandler> payoutHandlers,
BTCPayNetworkJsonSerializerSettings btcPayNetworkJsonSerializerSettings,
Logs logs)
LightningAddressService lightningAddressService,
ILogger<MigrationStartupTask> logger)
{
Logs = logs;
_DBContextFactory = dbContextFactory;
_StoreRepository = storeRepository;
_NetworkProvider = networkProvider;
@ -64,6 +72,8 @@ namespace BTCPayServer.Hosting
_appService = appService;
_payoutHandlers = payoutHandlers;
_btcPayNetworkJsonSerializerSettings = btcPayNetworkJsonSerializerSettings;
_lightningAddressService = lightningAddressService;
_logger = logger;
_userManager = userManager;
LightningOptions = lightningOptions;
}
@ -175,14 +185,68 @@ namespace BTCPayServer.Hosting
settings.LighingAddressSettingRename = true;
await _Settings.UpdateSetting(settings);
}
if (!settings.LighingAddressDatabaseMigration)
{
await MigrateLighingAddressDatabaseMigration();
settings.LighingAddressDatabaseMigration = true;
}
if (!settings.AddStoreToPayout)
{
await MigrateAddStoreToPayout();
settings.AddStoreToPayout = true;
await _Settings.UpdateSetting(settings);
}
}
catch (Exception ex)
{
Logs.PayServer.LogError(ex, "Error on the MigrationStartupTask");
_logger.LogError(ex, "Error on the MigrationStartupTask");
throw;
}
}
private async Task MigrateLighingAddressDatabaseMigration()
{
await using var ctx = _DBContextFactory.CreateContext();
var lightningAddressSettings =
await _Settings.GetSettingAsync<UILNURLController.LightningAddressSettings>(
nameof(UILNURLController.LightningAddressSettings));
if (lightningAddressSettings is null)
{
return;
}
var storeids = lightningAddressSettings.StoreToItemMap.Keys.ToArray();
var existingStores = (await ctx.Stores.Where(data => storeids.Contains(data.Id)).Select(data => data.Id ).ToArrayAsync()).ToHashSet();
foreach (var storeMap in lightningAddressSettings.StoreToItemMap)
{
if (!existingStores.Contains(storeMap.Key)) continue;
foreach (var storeitem in storeMap.Value)
{
if (lightningAddressSettings.Items.TryGetValue(storeitem, out var val))
{
await _lightningAddressService.Set(
new LightningAddressData()
{
StoreDataId = storeMap.Key,
Username = storeitem,
Blob = new LightningAddressDataBlob()
{
Max = val.Max,
Min = val.Min,
CurrencyCode = val.CurrencyCode
}.SerializeBlob()
}, ctx);
}
}
}
await ctx.SaveChangesAsync();
await _Settings.UpdateSetting<object>(null, nameof(UILNURLController.LightningAddressSettings));
}
private async Task MigrateLighingAddressSettingRename()
{
var old = await _Settings.GetSettingAsync<UILNURLController.LightningAddressSettings>("BTCPayServer.LNURLController+LightningAddressSettings");
@ -192,6 +256,41 @@ namespace BTCPayServer.Hosting
}
}
private async Task MigrateAddStoreToPayout()
{
await using var ctx = _DBContextFactory.CreateContext();
if (ctx.Database.IsNpgsql())
{
await ctx.Database.ExecuteSqlRawAsync(@"
WITH cte AS (
SELECT DISTINCT p.""Id"", pp.""StoreId"" FROM ""Payouts"" p
JOIN ""PullPayments"" pp ON pp.""Id"" = p.""PullPaymentDataId""
WHERE p.""StoreDataId"" IS NULL
)
UPDATE ""Payouts"" p
SET ""StoreDataId""=cte.""StoreId""
FROM cte
WHERE cte.""Id""=p.""Id""
");
}
else
{
var queryable = ctx.Payouts.Where(data => data.StoreDataId == null);
var count = await queryable.CountAsync();
_logger.LogInformation($"Migrating {count} payouts to have a store id explicitly");
for (int i = 0; i < count; i+=1000)
{
await queryable.Include(data => data.PullPaymentData).Skip(i).Take(1000)
.ForEachAsync(data => data.StoreDataId = data.PullPaymentData.StoreId);
await ctx.SaveChangesAsync();
_logger.LogInformation($"Migrated {i+1000}/{count} payouts to have a store id explicitly");
}
}
}
private async Task AddInitialUserBlob()
{
await using var ctx = _DBContextFactory.CreateContext();

View File

@ -113,6 +113,7 @@ namespace BTCPayServer.Hosting
services.AddScoped<Fido2Service>();
services.AddSingleton<UserLoginCodeService>();
services.AddSingleton<LnurlAuthService>();
services.AddSingleton<LightningAddressService>();
var mvcBuilder = services.AddMvc(o =>
{
o.Filters.Add(new XFrameOptionsAttribute("DENY"));

View File

@ -32,7 +32,7 @@ namespace BTCPayServer.ModelBinders
var networkProvider = (BTCPayNetworkProvider)bindingContext.HttpContext.RequestServices.GetService(typeof(BTCPayNetworkProvider));
var cryptoCode = bindingContext.ValueProvider.GetValue("cryptoCode").FirstValue;
var network = networkProvider.GetNetwork<BTCPayNetwork>(cryptoCode ?? "BTC");
var network = networkProvider.GetNetwork<BTCPayNetwork>(cryptoCode ?? networkProvider.DefaultNetwork.CryptoCode);
try
{
var data = network.NBXplorerNetwork.DerivationStrategyFactory.Parse(key);

View File

@ -7,4 +7,5 @@ public class AdditionalServiceViewModel
public string ServiceName { get; set; }
public string CryptoCode { get; set; }
public string Link { get; set; }
public string Error { get; set; }
}

View File

@ -7,10 +7,12 @@ namespace BTCPayServer.Models
{
public int Skip { get; set; } = 0;
public int Count { get; set; } = 50;
public int Total { get; set; }
public int? Total { get; set; }
[DisplayFormat(ConvertEmptyStringToNull = false)]
public string SearchTerm { get; set; }
public int? TimezoneOffset { get; set; }
public Dictionary<string, object> PaginationQuery { get; set; }
public abstract int CurrentPageCount { get; }
}
}

View File

@ -8,6 +8,7 @@ namespace BTCPayServer.Models.InvoicingModels
public class InvoicesModel : BasePagingViewModel
{
public List<InvoiceModel> Invoices { get; set; } = new List<InvoiceModel>();
public override int CurrentPageCount => Invoices.Count;
public string[] StoreIds { get; set; }
public string StoreId { get; set; }
public bool IncludeArchived { get; set; }

View File

@ -6,5 +6,7 @@ namespace BTCPayServer.Models.NotificationViewModels
public class IndexViewModel : BasePagingViewModel
{
public List<NotificationViewModel> Items { get; set; }
public override int CurrentPageCount => Items.Count;
}
}

View File

@ -13,6 +13,7 @@ namespace BTCPayServer.Models.PaymentRequestViewModels
public class ListPaymentRequestsViewModel : BasePagingViewModel
{
public List<ViewPaymentRequestViewModel> Items { get; set; }
public override int CurrentPageCount => Items.Count;
}
public class UpdatePaymentRequestViewModel

View File

@ -17,6 +17,7 @@ namespace BTCPayServer.Models.ServerViewModels
public IEnumerable<string> Roles { get; set; }
}
public List<UserViewModel> Users { get; set; } = new List<UserViewModel>();
public override int CurrentPageCount => Users.Count;
public Dictionary<string, string> Roles { get; set; }
}

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