Compare commits

..

85 Commits

Author SHA1 Message Date
4a31cf0a09 Migrate payment requests () 2024-10-01 16:07:51 +09:00
82620ee327 Move wallet payment settings back to store settings ()
Intermediate solution, until we implement these settings on the payment method level. Closes .
2024-09-30 19:13:51 +09:00
6d284b4124 Give time for pollers to detect payments after server restart 2024-09-27 15:48:16 +09:00
83fa8cbf0f prevent app creation without wallet creation ()
* prevent app creation without wallet creation

* resolve test failures

* resolve selenium test
2024-09-27 15:28:55 +09:00
9ba4b030ed Fix: Do not expose xpub without modify store permission () 2024-09-27 15:27:04 +09:00
272cc3d3c9 POS: Option for user sign in via the QR code ()
* Login Code: Turn into Blazor component and extend with data for the app

* POS: Add login code for POS frontend

* Improve components, fix test
2024-09-26 19:10:14 +09:00
b5590a38fe Add better error message if v1 routes are used. 2024-09-26 19:09:27 +09:00
443a350bad App Service: Validate IDs when parsing items template ()
Validates missing and duplicate IDs on the edit actions and when creating/updating apps via the API.
Fails gracefully by excluding existing items without ID or with duplicate ID for the rest of the cases.

Fixes .
2024-09-26 15:52:16 +09:00
7013e618de Remove dead fields from swagger 2024-09-26 12:23:41 +09:00
363b60385b Renaming various properties in the Payouts API ()
* Rename Payouts Currency/OriginalCurrency

* Rename Payout Processor PayoutMethodIds

* Rename paymentMethods to payoutMethodIds

* Rename payoutMethodIds to payoutMethods
2024-09-26 11:25:45 +09:00
90635ffc4e Remove BTCPAY_EXPERIMENTALV2_CONFIRM 2024-09-25 23:11:53 +09:00
056f850268 Optimize load time of StoreRoles related pages/routes () 2024-09-25 23:10:13 +09:00
336f2d88e9 Fix flaky CanManageWallet
Clicking on Sign Transaction in the Wallet Send page, will, when a hot
wallet is setup, use PostRedirect page to redirect to the
broadcast screen. The problem was that sometimes, s.Driver.PageSource
would return this PostRedirect page rather than the broadcast page.
Waiting for an element of the broadcast page fixes this issue.
2024-09-25 21:53:15 +09:00
e16b4062b5 Fix payout processor migration 2024-09-25 18:50:49 +09:00
747dacf3b1 Consolidate migrations from alpha () 2024-09-25 18:23:10 +09:00
f00a71922f Optimize queries from payout processor at startup 2024-09-24 23:39:05 +09:00
c97c9d4ece Add SQL test for GetMonitoredInvoices 2024-09-24 22:07:02 +09:00
9d3f8672d9 Fix GetMonitoredInvoices 2024-09-24 17:21:36 +09:00
fe48cd4236 fix InvoiceRepository.GetMonitoredInvoices () 2024-09-24 15:44:51 +09:00
587d3aa612 Fix query 2024-09-24 09:52:28 +09:00
8a951940fd Remove dead property 2024-09-24 09:47:46 +09:00
b726ef8a2e Migrate PayoutProcessors's PayoutMethodId in entity migration 2024-09-24 09:43:02 +09:00
25e360e175 Allow listeners to retrieve invoices with nonActivated prompts 2024-09-24 08:43:30 +09:00
1d9ec253fb Fix migration of Invoice's payment () 2024-09-23 23:59:18 +09:00
3cf1aa00fa Payments should use composite key ()
* Payments should use composite key

* Invert PK for InvoiceAddress
2024-09-23 17:06:56 +09:00
36a5d0ee3f Fix: Monero and ZCash not tracking addresses 2024-09-22 11:13:09 +09:00
f5e5174045 Refactor: Add GetMonitoredInvoices to fetch pending invoices or those with pending payments () 2024-09-20 18:54:36 +09:00
ba2301ebfe Refactor the InvoiceAddresses table () 2024-09-19 22:15:02 +09:00
df651a2157 Fix GetInvoicesWithPendingPayments 2024-09-18 18:11:30 +09:00
2d2c1d5f2d fix: check lightning payment status () 2024-09-17 21:41:04 +09:00
0f93581ff5 Refactor confirmation count tracking () 2024-09-17 17:28:58 +09:00
397452a7fe Setting standard 150x100 size for Unbank logo 2024-09-16 16:38:04 -05:00
cd3157361a Adding Unbank as BTCPay Server Foundation Supporter () 2024-09-16 16:33:11 -05:00
29a89f185a Fix: Not able to change SpeedPolicy of a store 2024-09-13 22:59:14 +09:00
2f7a5c2967 Wallet: Generate receive address automatically ()
* Wallet: Generate receive address automatically

This circumvents landing on a blank page with only the "generate address" button and automatically generates a new address, unless the Unreserve action was used.

* Fix close button leading to same page

* Fix tests

* Remove unreserve feature

---------

Co-authored-by: Nicolas Dorier <nicolas.dorier@gmail.com>
2024-09-13 22:03:45 +09:00
f07ed53f7e Handle password reset when SMTP isn't configured or validated ()
* Handle password reset when SMTP isn't configured or the configuration cannot be validated

* include rel in external a tag

* Simplify it

* Test fix

* Simplify a bit

* selenium test to manage users

---------

Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
Co-authored-by: Nicolas Dorier <nicolas.dorier@gmail.com>
2024-09-13 21:42:08 +09:00
7348a6a62f Store Branding: Apply brand color to backend as well ()
* Store Branding: Apply brand color to backend as well

Closes .

* Add adjustments for different theme scenarios

* Add description text

* Make it optional to apply the brand color to the backend

* Toggle color fixes
2024-09-13 21:39:21 +09:00
b7ba53eb60 UI: Fix for truncate center component () 2024-09-13 17:55:31 +09:00
e389d6a96b UI: Minor fix on policies page 2024-09-12 18:43:52 +02:00
0238dffc7a POS: Fix accounting for manually entered keypad amounts ()
* POS: Fix accounting for manually entered keypad amounts

For keypad orders where there are products AND manual amount entries, we didn't account for the latter.

Fixes .

* Adjust wording: "Manual entry" becomes "Custom Amount"
2024-09-12 21:36:35 +09:00
666445e8f7 Greenfield: App endpoints for sales statistics () 2024-09-12 16:17:16 +09:00
36bada8feb Uniformize Wallet API's path ()
* Uniformize Wallet API's path

* Rewrite old API path to new API

* Rename routes
2024-09-12 15:19:10 +09:00
b4946f4db1 Fix divisibility in invoice details of lightning amounts ()
* Fix divisibility in invoice details of lightning amounts

This PR will show 11 decimal in the invoice details for BTC amount
of lightning payment methods.

It also hacks around the fact that some
lightning clients don't create the requested amount of sats, which
resulted in over or under payments. (Blink not supporting msats, and
strike)

Now, In that case, a payment method fee (which can be negative) called tweak fee
will be added to the prompt.

We are also hiding this tweak fee from the user in the checkout page in
order to not disturb the UI with inconsequential fee of 0.000000001 sats.

* Only show 8 digits in checkout, even if amount is 11 digits
2024-09-12 12:43:08 +09:00
f3d485da53 Invitation process improvements ()
* Server: Make sending email optional when adding user

Closes .

* Generate custom invite token and store it in user blob

Closes btcpayserver/app/#46.

* QR code for user invite

Closes .

* Text fix
2024-09-12 12:31:57 +09:00
3342122be2 Make Role Permissions more human legible ()
Had to rename `CanModifyStoreWebhooks` to `CanModifyWebhooks` for this, but the value stayed the same, so I don't think it's a big deal.

Closes .
2024-09-12 12:29:10 +09:00
f59751853a Fx pos data additional info ()
* Resolve Additional Information from posData

* code formatting

* Minor adjustments

* Update ChromeDriver

* Revert and improve PosData partial

---------

Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
Co-authored-by: Nicolas Dorier <nicolas.dorier@gmail.com>
2024-09-12 12:27:02 +09:00
a60c55c6df Transactions: Improve TX ID display ()
* Transactions: Improve TX ID display

* Elastic fix

* Test and behaviour fix
2024-09-12 10:08:16 +09:00
3bad5883bb Permissions: Remove deprecated custodian account policies ()
Updates the store owner role and removes these three deprecated policies:

- `btcpay.store.cantradecustodianaccount`
- `btcpay.store.canwithdrawfromcustodianaccount`
- `btcpay.store.candeposittocustodianaccount`
2024-09-12 10:02:37 +09:00
d4c30866b7 Fix timeout issues during migration () 2024-09-10 20:11:07 +09:00
7c92ce771f Reindex Invoices table if corrupt, fix migration timeout () 2024-09-10 17:34:02 +09:00
4601359ebe Do not block the Lightning PendingPayoutListener if a client is failing 2024-09-10 17:33:36 +09:00
04b1130837 Fix tests 2024-09-09 23:39:59 +09:00
c377617b5a Fix migration issue on invalid json char 2024-09-09 23:15:53 +09:00
222e8f66df Fix entity half migrated entities for v2 2024-09-09 21:21:04 +09:00
87e2f5f414 Add migration logs 2024-09-09 19:03:07 +09:00
7de05700e9 Edit dictionary should be in JSON format () 2024-09-09 11:25:36 +09:00
841f41da2f Invoice: Improve zero amount invoice handling ()
This is for the checkout page to properly redirect paid invoices with no payment methods (e.g. free invoices with zero amount) to either the receipt page or redirect URL. Only fall back to 404 if there is neither.

Fixes .
2024-09-09 11:05:03 +09:00
73dcde7780 UI: Create Store CTA should be full-width
Closes 
2024-09-06 13:23:52 +02:00
377ff52222 Fix: Incorrect rounding for lightning amounts () 2024-09-06 18:48:20 +09:00
e4b5609d78 Fix doc 2024-09-06 13:27:12 +09:00
c93497af10 Rename PaymentMethod => PaymentMethodId ()
* Rename PaymentMethod => PaymentMethodId

* Rename DB Columns
2024-09-06 13:24:33 +09:00
99dda66bbc Rename PayoutData.Destination () 2024-09-06 10:34:10 +09:00
4ce68f817d UI: Spacing fixes for Policies page 2024-09-02 12:06:34 +02:00
1c027be106 Greenfield: Can set the label when generating a wallet for store () 2024-09-02 18:54:15 +09:00
4a94074595 Add topups to payouts () 2024-09-02 18:37:39 +09:00
b49f6c3f86 Checkout fixes and improvements ()
* Adjust to new payment method IDs

* Sound improvements

* NFC: Handle no data/empty tag case

Fixes .
2024-09-02 18:20:29 +09:00
9c8fe9277d Fix route's param in doc 2024-09-02 16:27:49 +09:00
a7c70e6912 Add missing doc for scriptPubKeyType TaprootBIP86 2024-09-02 10:35:06 +09:00
28156a5a6b Include hover to display total sales per day ()
* Include hover to display total sales per day

* Add label to top items as well

* Bring in recent Chart modifications from app

---------

Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
2024-08-30 09:58:50 +02:00
a3cfb9e5e4 Removal of the Altcoins build ()
* Remove some useless #if ALTCOINS

* Removal of the Altcoins build
2024-08-30 08:34:23 +09:00
25ccc6a9f9 Update notificaiton counter record () 2024-08-29 12:47:29 +02:00
8d1904c185 Sales report dashboard issue - querySelectorAll returns multiple elements that leads to not updateding the reuired element innerHtml directy, so i change querySelector that will directly update our .sales-count element innerHtml ()
Co-authored-by: Umar Farooq <umar.u.farooq@constellationdealer.com>
2024-08-29 11:30:34 +02:00
85cc221d50 Remove obsolete errors 2024-08-29 10:47:03 +09:00
588b00de45 Fix potential crash in migration 2024-08-29 10:21:25 +09:00
656a1f8294 bump version 2024-08-29 09:40:22 +09:00
1dd37c5020 Refactor Payouts and PullPayments DB models () 2024-08-28 18:52:08 +09:00
3c40dc1f49 Delete columns CustomerEmail, OrderId, ItemCode () 2024-08-27 09:53:28 +09:00
6d560caf06 Small optimization of query () 2024-08-22 18:52:53 +09:00
07f3301e32 Remove unused code () 2024-08-22 18:52:41 +09:00
9410a293f7 Fix tests 2024-08-22 17:21:06 +09:00
a0704eddb0 Fix tests 2024-08-22 16:34:37 +09:00
672491c66c UI: Offcanvas fixes 2024-08-20 18:12:32 +02:00
bd91c45d1f Minor design update 2024-08-20 18:12:21 +02:00
f77cdb7148 Removing Strike as BTCPay Foundation supporter and cleaning up old SVGs () 2024-08-11 13:21:09 -05:00
7878a4365c Dictionary additions ()
* Cleanups

* Add text entries for dictionary pages

* Wording: Keep Clone title consistent with Payment Request cloning

* Dictionaries: List used one first; badge for marking In use
2024-07-26 08:46:17 +09:00
428 changed files with 4931 additions and 8217 deletions
BTCPayApp.CommonServer
BTCPayServer.Abstractions
BTCPayServer.Client
BTCPayServer.Common
BTCPayServer.Data
BTCPayServer.Tests
BTCPayServer
App
BTCPayServer.csproj
Blazor
Components
Configuration
Controllers
BitpayAccessTokenController.csBitpayInvoiceController.csBitpayRateController.cs
GreenField
UIAccountController.csUIAppsController.Dashboard.csUIAppsController.csUIHomeController.csUIInvoiceController.UI.csUIInvoiceController.csUILNURLAuthController.csUILNURLController.csUIManageController.APIKeys.csUIManageController.LoginCodes.csUIManageController.csUINotificationsController.csUIPaymentRequestController.csUIPullPaymentController.Boltcard.csUIPullPaymentController.csUIReportsController.csUIServerController.Roles.csUIServerController.Translations.csUIServerController.Users.csUIServerController.csUIStorePullPaymentsController.PullPayments.csUIStoresController.Dashboard.csUIStoresController.Email.csUIStoresController.Integrations.csUIStoresController.LightningLike.csUIStoresController.Onchain.csUIStoresController.Rates.csUIStoresController.Roles.csUIStoresController.Settings.csUIStoresController.Tokens.csUIStoresController.Users.csUIStoresController.csUIUserStoresController.csUIVaultController.csUIWalletsController.cs
Data
Events
Extensions.cs
Extensions
Fido2
Forms
HostedServices
Hosting
Models
Payments
PayoutProcessors
Payouts
Plugins
Program.cs
Properties
Roles.cs
Security
Services
UserManagerExtensions.cs
Views
wwwroot
Build
README.mdbtcpayserver.sln

@ -1,16 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\BTCPayServer.Client\BTCPayServer.Client.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="8.0.6" />
</ItemGroup>
</Project>

@ -1,122 +0,0 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
using BTCPayServer.Lightning;
using NBitcoin;
namespace BTCPayApp.CommonServer;
//methods available on the hub in the client
public interface IBTCPayAppHubClient
{
Task NotifyServerEvent(ServerEvent ev);
Task NotifyNetwork(string network);
Task NotifyServerNode(string nodeInfo);
Task TransactionDetected(TransactionDetectedRequest request);
Task NewBlock(string block);
Task<LightningInvoice> CreateInvoice(CreateLightningInvoiceRequest createLightningInvoiceRequest);
Task<LightningInvoice?> GetLightningInvoice(uint256 paymentHash);
Task<LightningPayment?> GetLightningPayment(uint256 paymentHash);
Task<List<LightningPayment>> GetLightningPayments(ListPaymentsParams request);
Task<List<LightningInvoice>> GetLightningInvoices(ListInvoicesParams request);
Task<PayResponse> PayInvoice(string bolt11, long? amountMilliSatoshi);
Task MasterUpdated(long? deviceIdentifier);
Task<LightningNodeInformation> GetLightningNodeInfo();
Task<LightningNodeBalance> GetLightningBalance();
}
//methods available on the hub in the server
public interface IBTCPayAppHubServer
{
Task<bool> DeviceMasterSignal(long deviceIdentifier, bool active);
Task<Dictionary<string,string>> Pair(PairRequest request);
Task<AppHandshakeResponse> Handshake(AppHandshake request);
Task<bool> BroadcastTransaction(string tx);
Task<decimal> GetFeeRate(int blockTarget);
Task<BestBlockResponse> GetBestBlock();
Task<TxInfoResponse> FetchTxsAndTheirBlockHeads(string[] txIds);
Task<string> DeriveScript(string identifier);
Task TrackScripts(string identifier, string[] scripts);
Task<string> UpdatePsbt(string[] identifiers, string psbt);
Task<CoinResponse[]> GetUTXOs(string[] identifiers);
Task<Dictionary<string, TxResp[]>> GetTransactions(string[] identifiers);
Task SendInvoiceUpdate(string identifier, LightningInvoice lightningInvoice);
}
public class ServerEvent(string type)
{
public string Type { get; } = type;
public string? StoreId { get; init; }
public string? UserId { get; init; }
public string? InvoiceId { get; init; }
}
public record TxResp(long Confirmations, long? Height, decimal BalanceChange, DateTimeOffset Timestamp, string TransactionId)
{
public override string ToString()
{
return $"{{ Confirmations = {Confirmations}, Height = {Height}, BalanceChange = {BalanceChange}, Timestamp = {Timestamp}, TransactionId = {TransactionId} }}";
}
}
public class TransactionDetectedRequest
{
public string Identifier { get; set; }
public string TxId { get; set; }
public string[] SpentScripts { get; set; }
public string[] ReceivedScripts { get; set; }
public bool Confirmed { get; set; }
}
public class CoinResponse
{
public string Identifier{ get; set; }
public bool Confirmed { get; set; }
public string Script { get; set; }
public string Outpoint { get; set; }
public decimal Value { get; set; }
public string Path { get; set; }
}
public class TxInfoResponse
{
public Dictionary<string,TransactionResponse> Txs { get; set; }
public Dictionary<string,string> Blocks { get; set; }
public Dictionary<string,int> BlockHeghts { get; set; }
}
public class TransactionResponse
{
public string? BlockHash { get; set; }
public int? BlockHeight { get; set; }
public string Transaction { get; set; }
}
public class BestBlockResponse
{
public required string BlockHash { get; set; }
public required int BlockHeight { get; set; }
public string BlockHeader { get; set; }
}
public class AppHandshake
{
public string[] Identifiers { get; set; }
}
public class AppHandshakeResponse
{
//response about identifiers being tracked successfully
public string[] IdentifiersAcknowledged { get; set; }
}
public class PairRequest
{
public Dictionary<string, string?> Derivations { get; set; } = new();
}

@ -1,12 +0,0 @@
using System.ComponentModel.DataAnnotations;
namespace BTCPayApp.CommonServer.Models;
public class AcceptInviteRequest
{
[Required]
public string? UserId { get; init; }
[Required]
public string? Code { get; init; }
}

@ -1,9 +0,0 @@
namespace BTCPayApp.CommonServer.Models;
public class AcceptInviteResult(string email)
{
public string Email { get; init; } = email;
public bool? RequiresUserApproval { get; set; }
public bool? EmailHasBeenConfirmed { get; set; }
public string? PasswordSetCode { get; set; }
}

@ -1,10 +0,0 @@
using System;
namespace BTCPayApp.CommonServer.Models;
public class AccessTokenResult(string accessToken, string refreshToken, DateTimeOffset expiry)
{
public string AccessToken { get; init; } = accessToken;
public string RefreshToken { get; init; } = refreshToken;
public DateTimeOffset Expiry { get; init; } = expiry;
}

@ -1,12 +0,0 @@
namespace BTCPayApp.CommonServer.Models;
public class AppInstanceInfo
{
public string BaseUrl { get; set; }
public string ServerName { get; set; }
public string? ContactUrl { get; set; }
public string? LogoUrl { get; set; }
public string? CustomThemeCssUrl { get; set; }
public string? CustomThemeExtension { get; set; }
public bool RegistrationEnabled { get; set; }
}

@ -1,68 +0,0 @@
using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
using System.Text.Json;
using BTCPayServer.Lightning;
using NBitcoin;
using NBitcoin.JsonConverters;
using Newtonsoft.Json;
namespace BTCPayApp.CommonServer.Models;
// public partial class LightningPayment
// {
// public string PaymentHash { get; set; }
// public string? PaymentId { get; set; }
// public string? Preimage { get; set; }
// public string? Secret { get; set; }
// public bool Inbound { get; set; }
// public DateTimeOffset Timestamp { get; set; }
// public long Value { get; set; }
// public LightningPaymentStatus Status { get; set; }
//
// //you can have multiple requests generated for the same payment hash, but once you reveal the preimage, you should reject any attempt to pay the same payment hash
// public List<string> PaymentRequests { get; set; }
// [JsonIgnore]
// public Dictionary<string, JsonDocument> AdditionalData { get; set; }
//
// }
public class AppUserInfo
{
public string? UserId { get; set; }
public string? Email { get; set; }
public string? Name { get; set; }
public string? ImageUrl { get; set; }
public IEnumerable<string>? Roles { get; set; }
public IEnumerable<AppUserStoreInfo>? Stores { get; set; }
public void SetInfo(string email, string? name, string? imageUrl)
{
Email = email;
Name = name;
ImageUrl = imageUrl;
}
public static bool Equals(AppUserInfo? x, AppUserInfo? y)
{
if (ReferenceEquals(x, y)) return true;
if (ReferenceEquals(x, null)) return false;
if (ReferenceEquals(y, null)) return false;
if (x.GetType() != y.GetType()) return false;
return x.UserId == y.UserId && x.Email == y.Email &&
x.Name == y.Name && x.ImageUrl == y.ImageUrl &&
Equals(x.Roles, y.Roles) && Equals(x.Stores, y.Stores);
}
}
public class AppUserStoreInfo
{
public string? Id { get; set; }
public string? Name { get; set; }
public string? LogoUrl { get; set; }
public string? RoleId { get; set; }
public string? PosAppId { get; set; }
public string? DefaultCurrency { get; set; }
public bool Archived { get; set; }
public IEnumerable<string>? Permissions { get; set; }
}

@ -1,10 +0,0 @@
using System.Collections.Generic;
namespace BTCPayApp.CommonServer.Models;
public class CreateStoreData
{
public string? DefaultCurrency { get; set; }
public string? RecommendedExchangeId { get; set; }
public Dictionary<string, string>? Exchanges { get; set; }
}

@ -1,12 +0,0 @@
using System.ComponentModel.DataAnnotations;
namespace BTCPayApp.CommonServer.Models;
public class SignupRequest
{
[Required]
public string? Email { get; init; }
[Required]
public string? Password { get; init; }
}

@ -1,8 +0,0 @@
namespace BTCPayApp.CommonServer.Models;
public class SignupResult
{
public string? Email { get; set; }
public bool RequiresConfirmedEmail { get; set; }
public bool RequiresUserApproval { get; set; }
}

@ -1,49 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json.Linq;
namespace BTCPayApp.CommonServer;
public static class PosDataParser
{
public static Dictionary<string, object> ParsePosData(JToken? posData)
{
var result = new Dictionary<string, object>();
if (posData is JObject jobj)
{
foreach (var item in jobj)
{
ParsePosDataItem(item, ref result);
}
}
return result;
}
static void ParsePosDataItem(KeyValuePair<string, JToken?> item, ref Dictionary<string, object> result)
{
switch (item.Value?.Type)
{
case JTokenType.Array:
var items = item.Value.AsEnumerable().ToList();
var arrayResult = new List<object>();
for (var i = 0; i < items.Count; i++)
{
arrayResult.Add(items[i] is JObject
? ParsePosData(items[i])
: items[i].ToString());
}
result.TryAdd(item.Key, arrayResult);
break;
case JTokenType.Object:
result.TryAdd(item.Key, ParsePosData(item.Value));
break;
case null:
break;
default:
result.TryAdd(item.Key, item.Value.ToString());
break;
}
}
}

@ -1,14 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace BTCPayApp.CommonServer;
public class Roles
{
public const string ServerAdmin = "ServerAdmin";
public static bool HasServerAdmin(IList<string> roles)
{
return roles.Contains(Roles.ServerAdmin, StringComparer.Ordinal);
}
}

@ -36,7 +36,6 @@
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BTCPayApp.CommonServer\BTCPayApp.CommonServer.csproj" />
<ProjectReference Include="..\BTCPayServer.Client\BTCPayServer.Client.csproj" />
</ItemGroup>
</Project>

@ -1,12 +1,11 @@
namespace BTCPayApp.CommonServer
namespace BTCPayServer.Abstractions.Constants
{
public class AuthenticationSchemes
{
public const string Cookie = "Identity.Application";
public const string Bitpay = "Bitpay";
public const string Greenfield = "Greenfield.APIKeys,Greenfield.Basic,Greenfield.Bearer";
public const string Greenfield = "Greenfield.APIKeys,Greenfield.Basic";
public const string GreenfieldAPIKeys = "Greenfield.APIKeys";
public const string GreenfieldBasic = "Greenfield.Basic";
public const string GreenfieldBearer = "Greenfield.Bearer";
}
}

@ -1,4 +1,3 @@
using BTCPayApp.CommonServer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;

@ -1,7 +1,7 @@
using System;
using Microsoft.AspNetCore.Authorization;
namespace BTCPayApp.CommonServer
namespace BTCPayServer.Security
{
public class PolicyRequirement : IAuthorizationRequirement
{

@ -12,7 +12,7 @@
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<RepositoryUrl>https://github.com/btcpayserver/btcpayserver</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<Configurations>Debug;Release;Altcoins-Debug;Altcoins-Release</Configurations>
<Configurations>Debug;Release</Configurations>
<Platforms>AnyCPU</Platforms>
</PropertyGroup>
<PropertyGroup>

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
@ -58,6 +59,20 @@ public partial class BTCPayServerClient
return await SendHttpRequest<CrowdfundAppData>($"api/v1/apps/crowdfund/{appId}", null, HttpMethod.Get, token);
}
public virtual async Task<AppSalesStats> GetAppSales(string appId, int numberOfDays = 7, CancellationToken token = default)
{
if (appId == null) throw new ArgumentNullException(nameof(appId));
var queryPayload = new Dictionary<string, object> { { nameof(numberOfDays), numberOfDays } };
return await SendHttpRequest<AppSalesStats>($"api/v1/apps/{appId}/sales", queryPayload, HttpMethod.Get, token);
}
public virtual async Task<List<AppItemStats>> GetAppTopItems(string appId, int offset = 0, int count = 10, CancellationToken token = default)
{
if (appId == null) throw new ArgumentNullException(nameof(appId));
var queryPayload = new Dictionary<string, object> { { nameof(offset), offset }, { nameof(count), count } };
return await SendHttpRequest<List<AppItemStats>>($"api/v1/apps/{appId}/top-items", queryPayload, HttpMethod.Get, token);
}
public virtual async Task DeleteApp(string appId, CancellationToken token = default)
{
if (appId == null) throw new ArgumentNullException(nameof(appId));

@ -46,9 +46,15 @@ public partial class BTCPayServerClient
return await SendHttpRequest<InvoiceData>($"api/v1/stores/{storeId}/invoices/{invoiceId}", null, HttpMethod.Get, token);
}
public virtual async Task<InvoicePaymentMethodDataModel[]> GetInvoicePaymentMethods(string storeId, string invoiceId,
bool onlyAccountedPayments = true, bool includeSensitive = false,
CancellationToken token = default)
{
return await SendHttpRequest<InvoicePaymentMethodDataModel[]>($"api/v1/stores/{storeId}/invoices/{invoiceId}/payment-methods", null, HttpMethod.Get, token);
var queryPayload = new Dictionary<string, object>
{
{ nameof(onlyAccountedPayments), onlyAccountedPayments },
{ nameof(includeSensitive), includeSensitive }
};
return await SendHttpRequest<InvoicePaymentMethodDataModel[]>($"api/v1/stores/{storeId}/invoices/{invoiceId}/payment-methods", queryPayload, HttpMethod.Get, token);
}
public virtual async Task ArchiveInvoice(string storeId, string invoiceId,

@ -21,13 +21,6 @@ public partial class BTCPayServerClient
return await SendHttpRequest<LightningNodeBalanceData>($"api/v1/server/lightning/{cryptoCode}/balance", null, HttpMethod.Get, token);
}
public virtual async Task<HistogramData> GetLightningNodeHistogram(string cryptoCode, HistogramType? type = null,
CancellationToken token = default)
{
var queryPayload = type == null ? null : new Dictionary<string, string> { ["type"] = type.ToString() };
return await SendHttpRequest<HistogramData>($"api/v1/server/lightning/{cryptoCode}/histogram", queryPayload, HttpMethod.Get, token);
}
public virtual async Task ConnectToLightningNode(string cryptoCode, ConnectToNodeRequest request,
CancellationToken token = default)
{

@ -21,13 +21,6 @@ public partial class BTCPayServerClient
return await SendHttpRequest<LightningNodeBalanceData>($"api/v1/stores/{storeId}/lightning/{cryptoCode}/balance", null, HttpMethod.Get, token);
}
public virtual async Task<HistogramData> GetLightningNodeHistogram(string storeId, string cryptoCode, HistogramType? type = null,
CancellationToken token = default)
{
var queryPayload = type == null ? null : new Dictionary<string, string> { ["type"] = type.ToString() };
return await SendHttpRequest<HistogramData>($"api/v1/stores/{storeId}/lightning/{cryptoCode}/histogram", queryPayload, HttpMethod.Get, token);
}
public virtual async Task ConnectToLightningNode(string storeId, string cryptoCode, ConnectToNodeRequest request,
CancellationToken token = default)
{

@ -15,7 +15,7 @@ public partial class BTCPayServerClient
int amount = 10,
CancellationToken token = default)
{
return await SendHttpRequest<UpdatePaymentMethodRequest, OnChainPaymentMethodPreviewResultData>($"api/v1/stores/{storeId}/payment-methods/{paymentMethodId}/preview",
return await SendHttpRequest<UpdatePaymentMethodRequest, OnChainPaymentMethodPreviewResultData>($"api/v1/stores/{storeId}/payment-methods/{paymentMethodId}/wallet/preview",
new Dictionary<string, object> { { "offset", offset }, { "amount", amount } },
new UpdatePaymentMethodRequest { Config = JValue.CreateString(derivationScheme) },
HttpMethod.Post, token);
@ -25,7 +25,7 @@ public partial class BTCPayServerClient
string storeId, string paymentMethodId, int offset = 0, int amount = 10,
CancellationToken token = default)
{
return await SendHttpRequest<OnChainPaymentMethodPreviewResultData>($"api/v1/stores/{storeId}/payment-methods/{paymentMethodId}/preview",
return await SendHttpRequest<OnChainPaymentMethodPreviewResultData>($"api/v1/stores/{storeId}/payment-methods/{paymentMethodId}/wallet/preview",
new Dictionary<string, object> { { "offset", offset }, { "amount", amount } }, HttpMethod.Get, token);
}
@ -33,7 +33,7 @@ public partial class BTCPayServerClient
string paymentMethodId, GenerateOnChainWalletRequest request,
CancellationToken token = default)
{
return await SendHttpRequest<GenerateOnChainWalletResponse>($"api/v1/stores/{storeId}/payment-methods/{paymentMethodId}/generate", request, HttpMethod.Post, token);
return await SendHttpRequest<GenerateOnChainWalletResponse>($"api/v1/stores/{storeId}/payment-methods/{paymentMethodId}/wallet/generate", request, HttpMethod.Post, token);
}
}

@ -15,7 +15,7 @@ public partial class BTCPayServerClient
parameters.Add("includeNeighbourData", v);
try
{
return await SendHttpRequest<OnChainWalletObjectData>($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/objects/{objectId.Type}/{objectId.Id}", parameters, HttpMethod.Get, token);
return await SendHttpRequest<OnChainWalletObjectData>($"api/v1/stores/{storeId}/payment-methods/{cryptoCode}-CHAIN/wallet/objects/{objectId.Type}/{objectId.Id}", parameters, HttpMethod.Get, token);
}
catch (GreenfieldAPIException err) when (err.APIError.Code == "wallet-object-not-found")
{
@ -31,17 +31,17 @@ public partial class BTCPayServerClient
parameters.Add("ids", ids);
if (query?.IncludeNeighbourData is bool v)
parameters.Add("includeNeighbourData", v);
return await SendHttpRequest<OnChainWalletObjectData[]>($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/objects", parameters, HttpMethod.Get, token);
return await SendHttpRequest<OnChainWalletObjectData[]>($"api/v1/stores/{storeId}/payment-methods/{cryptoCode}-CHAIN/wallet/objects", parameters, HttpMethod.Get, token);
}
public virtual async Task RemoveOnChainWalletObject(string storeId, string cryptoCode, OnChainWalletObjectId objectId,
CancellationToken token = default)
{
await SendHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/objects/{objectId.Type}/{objectId.Id}", null, HttpMethod.Delete, token);
await SendHttpRequest($"api/v1/stores/{storeId}/payment-methods/{cryptoCode}-CHAIN/wallet/objects/{objectId.Type}/{objectId.Id}", null, HttpMethod.Delete, token);
}
public virtual async Task<OnChainWalletObjectData> AddOrUpdateOnChainWalletObject(string storeId, string cryptoCode, AddOnChainWalletObjectRequest request,
CancellationToken token = default)
{
return await SendHttpRequest<OnChainWalletObjectData>($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/objects", request, HttpMethod.Post, token);
return await SendHttpRequest<OnChainWalletObjectData>($"api/v1/stores/{storeId}/payment-methods/{cryptoCode}-CHAIN/wallet/objects", request, HttpMethod.Post, token);
}
public virtual async Task AddOrUpdateOnChainWalletLink(string storeId, string cryptoCode,
@ -49,7 +49,7 @@ public partial class BTCPayServerClient
AddOnChainWalletObjectLinkRequest request = null,
CancellationToken token = default)
{
await SendHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/objects/{objectId.Type}/{objectId.Id}/links", request, HttpMethod.Post, token);
await SendHttpRequest($"api/v1/stores/{storeId}/payment-methods/{cryptoCode}-CHAIN/wallet/objects/{objectId.Type}/{objectId.Id}/links", request, HttpMethod.Post, token);
}
public virtual async Task RemoveOnChainWalletLinks(string storeId, string cryptoCode,
@ -57,6 +57,6 @@ public partial class BTCPayServerClient
OnChainWalletObjectId link,
CancellationToken token = default)
{
await SendHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/objects/{objectId.Type}/{objectId.Id}/links/{link.Type}/{link.Id}", null, HttpMethod.Delete, token);
await SendHttpRequest($"api/v1/stores/{storeId}/payment-methods/{cryptoCode}-CHAIN/wallet/objects/{objectId.Type}/{objectId.Id}/links/{link.Type}/{link.Id}", null, HttpMethod.Delete, token);
}
}

@ -14,16 +14,8 @@ public partial class BTCPayServerClient
public virtual async Task<OnChainWalletOverviewData> ShowOnChainWalletOverview(string storeId, string cryptoCode,
CancellationToken token = default)
{
return await SendHttpRequest<OnChainWalletOverviewData>($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet", null, HttpMethod.Get, token);
return await SendHttpRequest<OnChainWalletOverviewData>($"api/v1/stores/{storeId}/payment-methods/{cryptoCode}-CHAIN/wallet", null, HttpMethod.Get, token);
}
public virtual async Task<HistogramData> GetOnChainWalletHistogram(string storeId, string cryptoCode, HistogramType? type = null,
CancellationToken token = default)
{
var queryPayload = type == null ? null : new Dictionary<string, string> { ["type"] = type.ToString() };
return await SendHttpRequest<HistogramData>($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/histogram", queryPayload, HttpMethod.Get, token);
}
public virtual async Task<OnChainWalletFeeRateData> GetOnChainFeeRate(string storeId, string cryptoCode, int? blockTarget = null,
CancellationToken token = default)
{
@ -32,13 +24,13 @@ public partial class BTCPayServerClient
{
queryParams.Add("blockTarget", blockTarget);
}
return await SendHttpRequest<OnChainWalletFeeRateData>($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/feeRate", queryParams, HttpMethod.Get, token);
return await SendHttpRequest<OnChainWalletFeeRateData>($"api/v1/stores/{storeId}/payment-methods/{cryptoCode}-CHAIN/wallet/feerate", queryParams, HttpMethod.Get, token);
}
public virtual async Task<OnChainWalletAddressData> GetOnChainWalletReceiveAddress(string storeId, string cryptoCode, bool forceGenerate = false,
CancellationToken token = default)
{
return await SendHttpRequest<OnChainWalletAddressData>($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/address", new Dictionary<string, object>
return await SendHttpRequest<OnChainWalletAddressData>($"api/v1/stores/{storeId}/payment-methods/{cryptoCode}-CHAIN/wallet/address", new Dictionary<string, object>
{
{"forceGenerate", forceGenerate}
}, HttpMethod.Get, token);
@ -47,7 +39,7 @@ public partial class BTCPayServerClient
public virtual async Task UnReserveOnChainWalletReceiveAddress(string storeId, string cryptoCode,
CancellationToken token = default)
{
await SendHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/address", null, HttpMethod.Delete, token);
await SendHttpRequest($"api/v1/stores/{storeId}/payment-methods/{cryptoCode}-CHAIN/wallet/address", null, HttpMethod.Delete, token);
}
public virtual async Task<IEnumerable<OnChainWalletTransactionData>> ShowOnChainWalletTransactions(
@ -67,14 +59,14 @@ public partial class BTCPayServerClient
{
query.Add(nameof(skip), skip);
}
return await SendHttpRequest<IEnumerable<OnChainWalletTransactionData>>($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/transactions", query, HttpMethod.Get, token);
return await SendHttpRequest<IEnumerable<OnChainWalletTransactionData>>($"api/v1/stores/{storeId}/payment-methods/{cryptoCode}-CHAIN/wallet/transactions", query, HttpMethod.Get, token);
}
public virtual async Task<OnChainWalletTransactionData> GetOnChainWalletTransaction(
string storeId, string cryptoCode, string transactionId,
CancellationToken token = default)
{
return await SendHttpRequest<OnChainWalletTransactionData>($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/transactions/{transactionId}", null, HttpMethod.Get, token);
return await SendHttpRequest<OnChainWalletTransactionData>($"api/v1/stores/{storeId}/payment-methods/{cryptoCode}-CHAIN/wallet/transactions/{transactionId}", null, HttpMethod.Get, token);
}
public virtual async Task<OnChainWalletTransactionData> PatchOnChainWalletTransaction(
@ -82,7 +74,7 @@ public partial class BTCPayServerClient
PatchOnChainTransactionRequest request,
bool force = false, CancellationToken token = default)
{
return await SendHttpRequest<PatchOnChainTransactionRequest, OnChainWalletTransactionData>($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/transactions/{transactionId}",
return await SendHttpRequest<PatchOnChainTransactionRequest, OnChainWalletTransactionData>($"api/v1/stores/{storeId}/payment-methods/{cryptoCode}-CHAIN/wallet/transactions/{transactionId}",
new Dictionary<string, object> { {"force", force} }, request, HttpMethod.Patch, token);
}
@ -90,7 +82,7 @@ public partial class BTCPayServerClient
string cryptoCode,
CancellationToken token = default)
{
return await SendHttpRequest<IEnumerable<OnChainWalletUTXOData>>($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/utxos", null, HttpMethod.Get, token);
return await SendHttpRequest<IEnumerable<OnChainWalletUTXOData>>($"api/v1/stores/{storeId}/payment-methods/{cryptoCode}-CHAIN/wallet/utxos", null, HttpMethod.Get, token);
}
public virtual async Task<OnChainWalletTransactionData> CreateOnChainTransaction(string storeId,
@ -102,7 +94,7 @@ public partial class BTCPayServerClient
throw new ArgumentOutOfRangeException(nameof(request.ProceedWithBroadcast),
"Please use CreateOnChainTransactionButDoNotBroadcast when wanting to only create the transaction");
}
return await SendHttpRequest<OnChainWalletTransactionData>($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/transactions", request, HttpMethod.Post, token);
return await SendHttpRequest<OnChainWalletTransactionData>($"api/v1/stores/{storeId}/payment-methods/{cryptoCode}-CHAIN/wallet/transactions", request, HttpMethod.Post, token);
}
public virtual async Task<Transaction> CreateOnChainTransactionButDoNotBroadcast(string storeId,
@ -114,6 +106,6 @@ public partial class BTCPayServerClient
throw new ArgumentOutOfRangeException(nameof(request.ProceedWithBroadcast),
"Please use CreateOnChainTransaction when wanting to also broadcast the transaction");
}
return Transaction.Parse(await SendHttpRequest<string>($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/transactions", request, HttpMethod.Post, token), network);
return Transaction.Parse(await SendHttpRequest<string>($"api/v1/stores/{storeId}/payment-methods/{cryptoCode}-CHAIN/wallet/transactions", request, HttpMethod.Post, token), network);
}
}

@ -0,0 +1,15 @@
using BTCPayServer.JsonConverters;
using Newtonsoft.Json;
namespace BTCPayServer.Client.Models;
public class AppItemStats
{
public string ItemCode { get; set; }
public string Title { get; set; }
public int SalesCount { get; set; }
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal Total { get; set; }
public string TotalFormatted { get; set; }
}

@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
namespace BTCPayServer.Client.Models;
public class AppSalesStats
{
public int SalesCount { get; set; }
public IEnumerable<AppSalesStatsItem> Series { get; set; }
}
public class AppSalesStatsItem
{
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
public DateTime Date { get; set; }
public string Label { get; set; }
public int SalesCount { get; set; }
}

@ -8,6 +8,6 @@ namespace BTCPayServer.Client.Models
public string Destination { get; set; }
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal? Amount { get; set; }
public string PaymentMethod { get; set; }
public string PayoutMethodId { get; set; }
}
}

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

@ -10,6 +10,7 @@ namespace BTCPayServer.Client
{
public class GenerateOnChainWalletRequest
{
public string Label { get; set; }
public int AccountNumber { get; set; } = 0;
[JsonConverter(typeof(MnemonicJsonConverter))]
public Mnemonic ExistingMnemonic { get; set; }
@ -29,6 +30,7 @@ namespace BTCPayServer.Client
{
public class ConfigData
{
public string Label { get; set; }
public string AccountDerivation { get; set; }
[JsonExtensionData]
IDictionary<string, JToken> AdditionalData { get; set; }

@ -6,7 +6,7 @@ namespace BTCPayServer.Client.Models;
public class LightningAutomatedPayoutSettings
{
public string PaymentMethod { get; set; }
public string PayoutMethodId { get; set; }
[JsonConverter(typeof(TimeSpanJsonConverter.Seconds))]
public TimeSpan IntervalSeconds { get; set; }

@ -6,7 +6,7 @@ namespace BTCPayServer.Client.Models;
public class OnChainAutomatedPayoutSettings
{
public string PaymentMethod { get; set; }
public string PayoutMethodId { get; set; }
[JsonConverter(typeof(TimeSpanJsonConverter.Seconds))]
public TimeSpan IntervalSeconds { get; set; }

@ -5,7 +5,7 @@ namespace BTCPayServer.Client.Models;
public class PaymentMethodCriteriaData
{
public string PaymentMethod { get; set; }
public string PaymentMethodId { get; set; }
public string CurrencyCode { get; set; }
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal Amount { get; set; }

@ -21,12 +21,13 @@ namespace BTCPayServer.Client.Models
public string Id { get; set; }
public string PullPaymentId { get; set; }
public string Destination { get; set; }
public string PaymentMethod { get; set; }
public string CryptoCode { get; set; }
public string PayoutMethodId { get; set; }
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal Amount { get; set; }
public decimal OriginalAmount { get; set; }
public string OriginalCurrency { get; set; }
public string PayoutCurrency { get; set; }
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal? PaymentMethodAmount { get; set; }
public decimal? PayoutAmount { get; set; }
[JsonConverter(typeof(StringEnumConverter))]
public PayoutState State { get; set; }
public int Revision { get; set; }

@ -4,6 +4,6 @@ namespace BTCPayServer.Client.Models
{
public string Name { get; set; }
public string FriendlyName { get; set; }
public string[] PaymentMethods { get; set; }
public string[] PayoutMethods { get; set; }
}
}

@ -17,7 +17,7 @@ namespace BTCPayServer.Client.Models
public class RefundInvoiceRequest
{
public string? Name { get; set; } = null;
public string? PaymentMethod { get; set; }
public string? PayoutMethodId { get; set; }
public string? Description { get; set; } = null;
[JsonConverter(typeof(StringEnumConverter))]

@ -17,6 +17,7 @@ namespace BTCPayServer.Client.Models
public string Website { get; set; }
public string BrandColor { get; set; }
public bool ApplyBrandColorToBackend { get; set; }
public string LogoUrl { get; set; }
public string CssUrl { get; set; }
public string PaymentSoundUrl { get; set; }

@ -1,24 +0,0 @@
using System;
using System.Collections.Generic;
using BTCPayServer.JsonConverters;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
namespace BTCPayServer.Client.Models;
public enum HistogramType
{
Week,
Month,
Year
}
public class HistogramData
{
[JsonConverter(typeof(StringEnumConverter))]
public HistogramType Type { get; set; }
public List<decimal> Series { get; set; }
public List<DateTimeOffset> Labels { get; set; }
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal Balance { get; set; }
}

@ -1,8 +1,10 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
using System.Linq.Expressions;
using System.Text.RegularExpressions;
namespace BTCPayServer.Client
{
@ -16,7 +18,7 @@ namespace BTCPayServer.Client
public const string CanUseLightningNodeInStore = "btcpay.store.canuselightningnode";
public const string CanModifyServerSettings = "btcpay.server.canmodifyserversettings";
public const string CanModifyStoreSettings = "btcpay.store.canmodifystoresettings";
public const string CanModifyStoreWebhooks = "btcpay.store.webhooks.canmodifywebhooks";
public const string CanModifyWebhooks = "btcpay.store.webhooks.canmodifywebhooks";
public const string CanModifyStoreSettingsUnscoped = "btcpay.store.canmodifystoresettings:";
public const string CanViewStoreSettings = "btcpay.store.canviewstoresettings";
public const string CanViewReports = "btcpay.store.canviewreports";
@ -48,7 +50,7 @@ namespace BTCPayServer.Client
yield return CanViewInvoices;
yield return CanCreateInvoice;
yield return CanModifyInvoices;
yield return CanModifyStoreWebhooks;
yield return CanModifyWebhooks;
yield return CanModifyServerSettings;
yield return CanModifyStoreSettings;
yield return CanViewStoreSettings;
@ -104,6 +106,16 @@ namespace BTCPayServer.Client
{
return policy.StartsWith("btcpay.user", StringComparison.OrdinalIgnoreCase);
}
private static readonly CultureInfo _culture = new (CultureInfo.InvariantCulture.Name);
public static string DisplayName(string policy)
{
var p = policy.Split(".");
if (p.Length < 3 || p[0] != "btcpay") return policy;
var constName = typeof(Policies).GetFields().Select(f => f.Name).FirstOrDefault(f => f.Equals(p[^1], StringComparison.OrdinalIgnoreCase));
var perm = string.IsNullOrEmpty(constName) ? string.Join(' ', p[2..]) : Regex.Replace(constName, "([A-Z])", " $1", RegexOptions.Compiled).Trim();
return $"{_culture.TextInfo.ToTitleCase(p[1])}: {_culture.TextInfo.ToTitleCase(perm)}";
}
}
public class PermissionSet
@ -247,7 +259,7 @@ namespace BTCPayServer.Client
Policies.CanManagePullPayments,
Policies.CanModifyInvoices,
Policies.CanViewStoreSettings,
Policies.CanModifyStoreWebhooks,
Policies.CanModifyWebhooks,
Policies.CanModifyPaymentRequests,
Policies.CanManagePayouts,
Policies.CanUseLightningNodeInStore);

@ -4,6 +4,8 @@ using System.Globalization;
using System.IO;
using System.Linq;
using BTCPayServer.Common;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.Extensions.DependencyInjection;
using NBitcoin;
using NBXplorer;
using NBXplorer.Models;
@ -131,7 +133,7 @@ namespace BTCPayServer
public string GetTrackedDestination(Script scriptPubKey)
{
return scriptPubKey.Hash.ToString() + "#" + CryptoCode.ToUpperInvariant();
return scriptPubKey.Hash.ToString();
}
}

@ -3,9 +3,13 @@ using System.Collections.Generic;
using System.Linq;
using BTCPayServer.Configuration;
using BTCPayServer.Logging;
using Microsoft.AspNetCore.DataProtection.KeyManagement;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using NBitcoin;
using NBXplorer;
using StandardConfiguration;
namespace BTCPayServer
{
@ -30,11 +34,6 @@ namespace BTCPayServer
Logs logs)
{
var networksList = networks.ToList();
#if !ALTCOINS
var onlyBTC = networksList.Count == 1 && networksList.First().IsBTC;
if (!onlyBTC)
throw new ConfigException($"This build of BTCPay Server does not support altcoins. Configured networks: {string.Join(',', networksList.Select(n => n.CryptoCode).ToArray())}");
#endif
_NBXplorerNetworkProvider = nbxplorerNetworkProvider;
NetworkType = nbxplorerNetworkProvider.NetworkType;
foreach (var network in networksList)

@ -1,13 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="../Build/Version.csproj" Condition="Exists('../Build/Version.csproj')" />
<Import Project="../Build/Common.csproj" />
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="NBXplorer.Client" Version="4.3.1" />
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="2.0.1" />
</ItemGroup>
<ItemGroup Condition="'$(Altcoins)' != 'true'">
<Compile Remove="Altcoins\**\*.cs"></Compile>
</ItemGroup>
<ItemGroup>
<Folder Include="Altcoins\" />
</ItemGroup>

@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;

@ -4,6 +4,7 @@ using System.Linq;
using BTCPayServer.Logging;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Bson;
namespace BTCPayServer
{
@ -11,17 +12,12 @@ namespace BTCPayServer
{
HashSet<string> chains = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
bool all = false;
public SelectedChains(IConfiguration configuration, Logs logs)
public SelectedChains(IConfiguration configuration)
{
foreach (var chain in (configuration["chains"] ?? "btc")
.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(t => t.ToUpperInvariant()))
{
if (new[] { "ETH", "USDT20", "FAU" }.Contains(chain, StringComparer.OrdinalIgnoreCase))
{
logs.Configuration.LogWarning($"'{chain}' is not anymore supported, please remove it from 'chains'");
continue;
}
if (chain == "*")
{
all = true;

@ -1,32 +0,0 @@
using BTCPayServer.Data;
using Laraue.EfCoreTriggers.Common.Extensions;
using Microsoft.EntityFrameworkCore;
namespace BTCPayServer.App.BackupStorage;
public class AppStorageItemData
{
public string Key { get; set; }
public long Version { get; set; }
public byte[] Value { get; set; }
public string UserId { get; set; }
public static void OnModelCreating(ModelBuilder builder)
{
builder.Entity<AppStorageItemData>()
.HasKey(o => new {o.Key, o.Version, o.UserId});
builder.Entity<AppStorageItemData>()
.HasIndex(o => new {o.Key, o.UserId}).IsUnique();
builder.Entity<ApplicationUser>().HasMany(user => user.AppStorageItems).WithOne(data => data.User)
.OnDelete(DeleteBehavior.Cascade);
builder.Entity<AppStorageItemData>()
.BeforeInsert(trigger => trigger
.Action(group => group
.Delete<AppStorageItemData>((@ref, entity) => @ref.New.UserId == entity.UserId && @ref.New.Key == entity.Key &&
@ref.New.Version > entity.Version)));
}
public ApplicationUser User { get; set; }
}

@ -1,8 +1,6 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.App.BackupStorage;
using Laraue.EfCoreTriggers.PostgreSql.Extensions;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
@ -17,7 +15,6 @@ namespace BTCPayServer.Data
var builder = new DbContextOptionsBuilder<ApplicationDbContext>();
// Same as launchsettings.json, it's connecting to the docker's postgres.
builder.UseNpgsql("User ID=postgres;Include Error Detail=true;Host=127.0.0.1;Port=39372;Database=btcpayserver");
builder.UsePostgreSqlTriggers();
return new ApplicationDbContext(builder.Options);
}
}
@ -28,8 +25,6 @@ namespace BTCPayServer.Data
{
}
public DbSet<AddressInvoiceData> AddressInvoices { get; set; }
public DbSet<AppStorageItemData> AppStorageItems { get; set; }
public DbSet<APIKeyData> ApiKeys { get; set; }
public DbSet<AppData> Apps { get; set; }
public DbSet<StoredFile> Files { get; set; }
@ -44,7 +39,6 @@ namespace BTCPayServer.Data
public DbSet<PaymentRequestData> PaymentRequests { get; set; }
public DbSet<PaymentData> Payments { get; set; }
public DbSet<PayoutData> Payouts { get; set; }
public DbSet<PendingInvoiceData> PendingInvoices { get; set; }
public DbSet<PlannedTransaction> PlannedTransactions { get; set; }
public DbSet<PullPaymentData> PullPayments { get; set; }
public DbSet<RefundData> Refunds { get; set; }
@ -73,7 +67,7 @@ namespace BTCPayServer.Data
base.OnModelCreating(builder);
// some of the data models don't have OnModelCreating for now, commenting them
AppStorageItemData.OnModelCreating(builder);
ApplicationUser.OnModelCreating(builder, Database);
AddressInvoiceData.OnModelCreating(builder);
APIKeyData.OnModelCreating(builder, Database);
@ -88,9 +82,8 @@ namespace BTCPayServer.Data
PairingCodeData.OnModelCreating(builder);
//PayjoinLock.OnModelCreating(builder);
PaymentRequestData.OnModelCreating(builder, Database);
PaymentData.OnModelCreating(builder, Database);
PaymentData.OnModelCreating(builder);
PayoutData.OnModelCreating(builder, Database);
PendingInvoiceData.OnModelCreating(builder);
//PlannedTransaction.OnModelCreating(builder);
PullPaymentData.OnModelCreating(builder, Database);
RefundData.OnModelCreating(builder);

@ -1,10 +1,8 @@
using System;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Abstractions.Models;
using Laraue.EfCoreTriggers.PostgreSql.Extensions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure;
@ -12,20 +10,20 @@ namespace BTCPayServer.Data
{
public class ApplicationDbContextFactory : BaseDbContextFactory<ApplicationDbContext>
{
public ApplicationDbContextFactory(IOptions<DatabaseOptions> options) : base(options, "")
public ApplicationDbContextFactory(IOptions<DatabaseOptions> options, ILoggerFactory loggerFactory) : base(options, "")
{
LoggerFactory = loggerFactory;
}
public ILoggerFactory LoggerFactory { get; }
public override ApplicationDbContext CreateContext(Action<NpgsqlDbContextOptionsBuilder> npgsqlOptionsAction = null)
{
var builder = new DbContextOptionsBuilder<ApplicationDbContext>();
builder.AddInterceptors(Data.InvoiceData.MigrationInterceptor.Instance);
builder.UseLoggerFactory(LoggerFactory);
builder.AddInterceptors(MigrationInterceptor.Instance);
ConfigureBuilder(builder, npgsqlOptionsAction);
builder.UsePostgreSqlTriggers();
return new ApplicationDbContext(builder.Options);
}
}
}

@ -3,7 +3,6 @@
<Import Project="../Build/Common.csproj" />
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Laraue.EfCoreTriggers.PostgreSql" Version="8.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.6">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@ -18,4 +17,10 @@
<ItemGroup>
<EmbeddedResource Include="DBScripts\*.sql" />
</ItemGroup>
<ItemGroup>
<None Remove="DBScripts\001.InvoiceFunctions.sql" />
<None Remove="DBScripts\002.RefactorPayouts.sql" />
<None Remove="DBScripts\003.RefactorPendingInvoicesPayments.sql" />
<None Remove="DBScripts\004.MonitoredInvoices.sql" />
</ItemGroup>
</Project>

@ -0,0 +1,12 @@
CREATE OR REPLACE FUNCTION get_orderid(invoice_blob jsonb)
RETURNS text AS $$
SELECT invoice_blob->'metadata'->>'orderId';
$$ LANGUAGE sql IMMUTABLE;
CREATE OR REPLACE FUNCTION get_itemcode(invoice_blob jsonb)
RETURNS text AS $$
SELECT invoice_blob->'metadata'->>'itemCode';
$$ LANGUAGE sql IMMUTABLE;
CREATE INDEX IF NOT EXISTS "IX_Invoices_Metadata_OrderId" ON "Invoices" (get_orderid("Blob2")) WHERE get_orderid("Blob2") IS NOT NULL;
CREATE INDEX IF NOT EXISTS "IX_Invoices_Metadata_ItemCode" ON "Invoices" (get_itemcode("Blob2")) WHERE get_itemcode("Blob2") IS NOT NULL;

@ -0,0 +1,62 @@
-- Rename column
ALTER TABLE "Payouts" RENAME COLUMN "PaymentMethodId" TO "PayoutMethodId";
-- Add Currency column, guessed from the PaymentMethodId
ALTER TABLE "Payouts" ADD COLUMN "Currency" TEXT;
UPDATE "Payouts" SET
"Currency" = split_part("PayoutMethodId", '_', 1),
"PayoutMethodId"=
CASE
WHEN ("Blob"->>'Amount')::NUMERIC < 0 THEN 'TOPUP'
WHEN split_part("PayoutMethodId", '_', 2) = 'LightningLike' THEN split_part("PayoutMethodId", '_', 1) || '-LN'
ELSE split_part("PayoutMethodId", '_', 1) || '-CHAIN'
END;
ALTER TABLE "Payouts" ALTER COLUMN "Currency" SET NOT NULL;
-- Remove Currency and Limit from PullPayment Blob, and put it into the columns in the table
ALTER TABLE "PullPayments" ADD COLUMN "Currency" TEXT;
UPDATE "PullPayments" SET "Currency" = "Blob"->>'Currency';
ALTER TABLE "PullPayments" ALTER COLUMN "Currency" SET NOT NULL;
ALTER TABLE "PullPayments" ADD COLUMN "Limit" NUMERIC;
UPDATE "PullPayments" SET "Limit" = ("Blob"->>'Limit')::NUMERIC;
ALTER TABLE "PullPayments" ALTER COLUMN "Limit" SET NOT NULL;
-- Remove unused properties, rename SupportedPaymentMethods, and fix legacy payment methods IDs
UPDATE "PullPayments" SET
"Blob" = jsonb_set(
"Blob" - 'SupportedPaymentMethods' - 'Limit' - 'Currency' - 'Period',
'{SupportedPayoutMethods}',
(SELECT jsonb_agg(to_jsonb(
CASE
WHEN split_part(value::TEXT, '_', 2) = 'LightningLike' THEN split_part(value::TEXT, '_', 1) || '-LN'
ELSE split_part(value::TEXT, '_', 1) || '-CHAIN'
END))
FROM jsonb_array_elements_text("Blob"->'SupportedPaymentMethods') AS value
));
--Remove "Amount" and "CryptoAmount" from Payout Blob, and put it into the columns in the table
-- Respectively "OriginalAmount" and "Amount"
ALTER TABLE "Payouts" ADD COLUMN "Amount" NUMERIC;
UPDATE "Payouts" SET "Amount" = ("Blob"->>'CryptoAmount')::NUMERIC;
ALTER TABLE "Payouts" ADD COLUMN "OriginalAmount" NUMERIC;
UPDATE "Payouts" SET "OriginalAmount" = ("Blob"->>'Amount')::NUMERIC;
ALTER TABLE "Payouts" ALTER COLUMN "OriginalAmount" SET NOT NULL;
ALTER TABLE "Payouts" ADD COLUMN "OriginalCurrency" TEXT;
UPDATE "Payouts" p
SET
"OriginalCurrency" = "Currency",
"Blob" = "Blob" - 'Amount' - 'CryptoAmount'
WHERE "PullPaymentDataId" IS NULL AND "OriginalCurrency" IS NULL;
UPDATE "Payouts" p
SET
"OriginalCurrency" = pp."Currency"
FROM "PullPayments" pp
WHERE "OriginalCurrency" IS NULL AND pp."Id"=p."PullPaymentDataId";
ALTER TABLE "Payouts" ALTER COLUMN "OriginalCurrency" SET NOT NULL;

@ -0,0 +1,9 @@
CREATE OR REPLACE FUNCTION is_pending(status TEXT)
RETURNS BOOLEAN AS $$
SELECT status = 'Processing' OR status = 'New';
$$ LANGUAGE sql IMMUTABLE;
CREATE INDEX "IX_Invoices_Pending" ON "Invoices"((1)) WHERE is_pending("Status");
CREATE INDEX "IX_Payments_Pending" ON "Payments"((1)) WHERE is_pending("Status");
DROP TABLE "PendingInvoices";
ANALYZE "Invoices";

@ -0,0 +1,23 @@
CREATE OR REPLACE FUNCTION get_prompt(invoice_blob JSONB, payment_method_id TEXT)
RETURNS JSONB AS $$
SELECT invoice_blob->'prompts'->payment_method_id
$$ LANGUAGE sql IMMUTABLE;
CREATE OR REPLACE FUNCTION get_monitored_invoices(arg_payment_method_id TEXT, include_non_activated BOOLEAN)
RETURNS TABLE (invoice_id TEXT, payment_id TEXT, payment_method_id TEXT) AS $$
WITH cte AS (
-- Get all the invoices which are pending. Even if no payments.
SELECT i."Id" invoice_id, p."Id" payment_id, p."PaymentMethodId" payment_method_id FROM "Invoices" i LEFT JOIN "Payments" p ON i."Id" = p."InvoiceDataId"
WHERE is_pending(i."Status")
UNION ALL
-- For invoices not pending, take all of those which have pending payments
SELECT i."Id" invoice_id, p."Id" payment_id, p."PaymentMethodId" payment_method_id FROM "Invoices" i INNER JOIN "Payments" p ON i."Id" = p."InvoiceDataId"
WHERE is_pending(p."Status") AND NOT is_pending(i."Status"))
SELECT cte.* FROM cte
JOIN "Invoices" i ON cte.invoice_id=i."Id"
LEFT JOIN "Payments" p ON cte.payment_id=p."Id" AND cte.payment_method_id=p."PaymentMethodId"
WHERE (p."PaymentMethodId" IS NOT NULL AND p."PaymentMethodId" = arg_payment_method_id) OR
(p."PaymentMethodId" IS NULL AND get_prompt(i."Blob2", arg_payment_method_id) IS NOT NULL AND
(include_non_activated IS TRUE OR (get_prompt(i."Blob2", arg_payment_method_id)->'inactive')::BOOLEAN IS NOT TRUE));
$$ LANGUAGE SQL STABLE;

@ -9,6 +9,7 @@ namespace BTCPayServer.Data
public string Address { get; set; }
public InvoiceData InvoiceData { get; set; }
public string InvoiceDataId { get; set; }
public string PaymentMethodId { get; set; }
internal static void OnModelCreating(ModelBuilder builder)
@ -18,7 +19,7 @@ namespace BTCPayServer.Data
.WithMany(i => i.AddressInvoices).OnDelete(DeleteBehavior.Cascade);
builder.Entity<AddressInvoiceData>()
#pragma warning disable CS0618
.HasKey(o => o.Address);
.HasKey(o => new { o.Address, o.PaymentMethodId });
#pragma warning restore CS0618
}
}

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using BTCPayServer.App.BackupStorage;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@ -30,9 +29,6 @@ namespace BTCPayServer.Data
public List<IdentityUserRole<string>> UserRoles { get; set; }
public List<AppStorageItemData> AppStorageItems { get; set; }
public static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
{
builder.Entity<ApplicationUser>()
@ -50,5 +46,6 @@ namespace BTCPayServer.Data
public bool ShowInvoiceStatusChangeHint { get; set; }
public string ImageUrl { get; set; }
public string Name { get; set; }
public string InvitationToken { get; set; }
}
}

@ -15,36 +15,13 @@ using Microsoft.EntityFrameworkCore.Diagnostics;
using BTCPayServer.Migrations;
using Newtonsoft.Json.Serialization;
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
using System.Threading.Tasks;
using System.Threading;
namespace BTCPayServer.Data
{
public partial class InvoiceData
public partial class InvoiceData : MigrationInterceptor.IHasMigration
{
/// <summary>
/// We have a migration running in the background that will migrate the data from the old blob to the new blob
/// Meanwhile, we need to make sure that invoices which haven't been migrated yet are migrated on the fly.
/// </summary>
public class MigrationInterceptor : IMaterializationInterceptor
{
public static readonly MigrationInterceptor Instance = new MigrationInterceptor();
public object InitializedInstance(MaterializationInterceptionData materializationData, object entity)
{
if (entity is InvoiceData invoiceData && invoiceData.Currency is null)
{
invoiceData.Migrate();
}
else if (entity is PaymentData paymentData && paymentData.Currency is null)
{
paymentData.Migrate();
}
else if (entity is PayoutData payoutData && payoutData.Currency is null)
{
payoutData.Migrate();
}
return entity;
}
}
static HashSet<string> superflousProperties = new HashSet<string>()
{
"availableAddressHashes",
@ -78,13 +55,14 @@ namespace BTCPayServer.Data
};
#pragma warning disable CS0618 // Type or member is obsolete
public void Migrate()
public bool TryMigrate()
{
if (Currency is not null)
return;
return false;
if (Blob is not (null or { Length: 0 }))
{
Blob2 = MigrationExtensions.Unzip(Blob);
Blob2 = MigrationExtensions.SanitizeJSON(Blob2);
Blob = null;
}
var blob = JObject.Parse(Blob2);
@ -232,11 +210,10 @@ namespace BTCPayServer.Data
}
blob.ConvertNumberToString("price");
Currency = blob["currency"].Value<string>();
Currency = blob["currency"].Value<string>().ToUpperInvariant();
var isTopup = blob["type"]?.Value<string>() is "TopUp";
var amount = decimal.Parse(blob["price"].Value<string>(), CultureInfo.InvariantCulture);
Amount = isTopup && amount == 0 ? null : decimal.Parse(blob["price"].Value<string>(), CultureInfo.InvariantCulture);
CustomerEmail = null;
foreach (var prop in superflousProperties)
blob.Property(prop)?.Remove();
if (blob["speedPolicy"] is JValue { Type: JTokenType.Integer, Value: 0 or 0L })
@ -373,7 +350,11 @@ namespace BTCPayServer.Data
};
blob["version"] = 3;
Blob2 = blob.ToString(Formatting.None);
return true;
}
[NotMapped]
public bool Migrated { get; set; }
static string[] detailsRemoveDefault =
[
"paymentMethodFeeRate",

@ -3,6 +3,8 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Data
{
@ -20,18 +22,17 @@ namespace BTCPayServer.Data
[Obsolete("Use Blob2 instead")]
public byte[] Blob { get; set; }
public string Blob2 { get; set; }
public string ItemCode { get; set; }
public string OrderId { get; set; }
public string Status { get; set; }
public string ExceptionStatus { get; set; }
[Obsolete("Unused")]
public string CustomerEmail { get; set; }
public List<AddressInvoiceData> AddressInvoices { get; set; }
public bool Archived { get; set; }
public List<PendingInvoiceData> PendingInvoices { get; set; }
public List<InvoiceSearchData> InvoiceSearchData { get; set; }
public List<RefundData> Refunds { get; set; }
public static string GetOrderId(string blob) => throw new NotSupportedException();
public static string GetItemCode(string blob) => throw new NotSupportedException();
public static bool IsPending(string status) => throw new NotSupportedException();
[Timestamp]
// With this, update of InvoiceData will fail if the row was modified by another process
public uint XMin { get; set; }
@ -41,7 +42,6 @@ namespace BTCPayServer.Data
.HasOne(o => o.StoreData)
.WithMany(a => a.Invoices).OnDelete(DeleteBehavior.Cascade);
builder.Entity<InvoiceData>().HasIndex(o => o.StoreDataId);
builder.Entity<InvoiceData>().HasIndex(o => o.OrderId);
builder.Entity<InvoiceData>().HasIndex(o => o.Created);
builder.Entity<InvoiceData>()
.Property(o => o.Blob2)
@ -49,6 +49,9 @@ namespace BTCPayServer.Data
builder.Entity<InvoiceData>()
.Property(o => o.Amount)
.HasColumnType("NUMERIC");
builder.HasDbFunction(typeof(InvoiceData).GetMethod(nameof(GetOrderId), new[] { typeof(string) }), b => b.HasName("get_orderid"));
builder.HasDbFunction(typeof(InvoiceData).GetMethod(nameof(GetItemCode), new[] { typeof(string) }), b => b.HasName("get_itemcode"));
builder.HasDbFunction(typeof(InvoiceData).GetMethod(nameof(IsPending), new[] { typeof(string) }), b => b.HasName("is_pending"));
}
}
}

@ -147,5 +147,8 @@ namespace BTCPayServer.Data
return $"{splitted[0]}-CHAIN";
throw new NotSupportedException("Unknown payment id " + paymentMethodId);
}
// Make postgres happy
public static string SanitizeJSON(string json) => json.Replace("\\u0000", string.Empty, StringComparison.OrdinalIgnoreCase);
}
}

@ -0,0 +1,45 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.EntityFrameworkCore;
namespace BTCPayServer.Data
{
/// <summary>
/// We have a migration running in the background that will migrate the data from the old blob to the new blob
/// Meanwhile, we need to make sure that invoices which haven't been migrated yet are migrated on the fly.
/// </summary>
public class MigrationInterceptor : IMaterializationInterceptor, ISaveChangesInterceptor
{
public interface IHasMigration
{
bool TryMigrate();
bool Migrated { get; set; }
}
public static readonly MigrationInterceptor Instance = new MigrationInterceptor();
public object InitializedInstance(MaterializationInterceptionData materializationData, object entity)
{
if (entity is IHasMigration hasMigration && hasMigration.TryMigrate())
{
hasMigration.Migrated = true;
}
return entity;
}
public ValueTask<InterceptionResult<int>> SavingChangesAsync(DbContextEventData eventData, InterceptionResult<int> result, CancellationToken cancellationToken = default(CancellationToken))
{
foreach (var entry in eventData.Context.ChangeTracker.Entries())
{
if (entry is { Entity: IHasMigration { Migrated: true }, State: EntityState.Modified })
// It seems doing nothing, but this actually set all properties as modified
entry.State = EntityState.Modified;
}
return new ValueTask<InterceptionResult<int>>(result);
}
}
}

@ -1,10 +1,13 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Reflection.Metadata;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Migrations;
using Microsoft.EntityFrameworkCore.ChangeTracking.Internal;
using NBitcoin;
using NBitcoin.Altcoins;
using NBitcoin.DataEncoders;
@ -13,16 +16,17 @@ using Newtonsoft.Json.Linq;
namespace BTCPayServer.Data
{
public partial class PaymentData
public partial class PaymentData : MigrationInterceptor.IHasMigration
{
public void Migrate()
public bool TryMigrate()
{
#pragma warning disable CS0618 // Type or member is obsolete
if (Currency is not null)
return;
return false;
if (Blob is not (null or { Length: 0 }))
{
Blob2 = MigrationExtensions.Unzip(Blob);
Blob2 = MigrationExtensions.SanitizeJSON(Blob2);
Blob = null;
}
var blob = JObject.Parse(Blob2);
@ -42,9 +46,10 @@ namespace BTCPayServer.Data
}
var cryptoCode = blob["cryptoCode"].Value<string>();
Type = cryptoCode + "_" + blob["cryptoPaymentDataType"].Value<string>();
Type = MigrationExtensions.MigratePaymentMethodId(Type);
var divisibility = MigrationExtensions.GetDivisibility(Type);
MigratedPaymentMethodId = PaymentMethodId;
PaymentMethodId = cryptoCode + "_" + blob["cryptoPaymentDataType"].Value<string>();
PaymentMethodId = MigrationExtensions.MigratePaymentMethodId(PaymentMethodId);
var divisibility = MigrationExtensions.GetDivisibility(PaymentMethodId);
Currency = blob["cryptoCode"].Value<string>();
blob.Remove("cryptoCode");
blob.Remove("cryptoPaymentDataType");
@ -157,7 +162,13 @@ namespace BTCPayServer.Data
Blob2 = blob.ToString(Formatting.None);
Accounted = null;
#pragma warning restore CS0618 // Type or member is obsolete
return true;
}
[NotMapped]
public bool Migrated { get; set; }
[NotMapped]
[EditorBrowsable(EditorBrowsableState.Never)]
public string MigratedPaymentMethodId { get; set; }
static readonly DateTimeOffset unixRef = new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero);
public static long DateTimeToMilliUnixTime(in DateTime time)

@ -1,4 +1,5 @@
using System;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@ -26,13 +27,15 @@ namespace BTCPayServer.Data
[Obsolete("Use Blob2 instead")]
public byte[] Blob { get; set; }
public string Blob2 { get; set; }
public string Type { get; set; }
public string PaymentMethodId { get; set; }
[Obsolete("Use Status instead")]
public bool? Accounted { get; set; }
public PaymentStatus? Status { get; set; }
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
public static bool IsPending(PaymentStatus? status) => throw new NotSupportedException();
internal static void OnModelCreating(ModelBuilder builder)
{
builder.Entity<PaymentData>()
.HasKey(o => new { o.Id, o.PaymentMethodId });
builder.Entity<PaymentData>()
.HasOne(o => o.InvoiceData)
.WithMany(i => i.Payments).OnDelete(DeleteBehavior.Cascade);
@ -47,6 +50,7 @@ namespace BTCPayServer.Data
builder.Entity<PaymentData>()
.Property(o => o.Amount)
.HasColumnType("NUMERIC");
builder.HasDbFunction(typeof(PaymentData).GetMethod(nameof(IsPending), new[] { typeof(PaymentStatus?) }), b => b.HasName("is_pending"));
}
}
}

@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Reflection.Metadata;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Data
{
public partial class PaymentRequestData : MigrationInterceptor.IHasMigration
{
[NotMapped]
public bool Migrated { get; set; }
public bool TryMigrate()
{
#pragma warning disable CS0618 // Type or member is obsolete
if (Blob is null && Blob2 is not null)
return false;
if (Blob2 is null)
{
Blob2 = Blob is not (null or { Length: 0 }) ? MigrationExtensions.Unzip(Blob) : "{}";
Blob2 = MigrationExtensions.SanitizeJSON(Blob2);
}
Blob = null;
#pragma warning restore CS0618 // Type or member is obsolete
var jobj = JObject.Parse(Blob2);
// Fixup some legacy payment requests
if (jobj["expiryDate"].Type == JTokenType.Date)
{
jobj["expiryDate"] = new JValue(NBitcoin.Utils.DateTimeToUnixTime(jobj["expiryDate"].Value<DateTime>()));
Blob2 = jobj.ToString(Newtonsoft.Json.Formatting.None);
}
return true;
}
}
}

@ -4,7 +4,7 @@ using Microsoft.EntityFrameworkCore.Infrastructure;
namespace BTCPayServer.Data
{
public class PaymentRequestData : IHasBlobUntyped
public partial class PaymentRequestData : IHasBlobUntyped
{
public string Id { get; set; }
public DateTimeOffset Created { get; set; }

@ -1,18 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BTCPayServer.Data
{
public partial class PayoutData
{
public void Migrate()
{
PayoutMethodId = MigrationExtensions.MigratePaymentMethodId(PayoutMethodId);
// Could only be BTC-LN or BTC-CHAIN, so we extract the crypto currency
Currency = PayoutMethodId.Split('-')[0];
}
}
}

@ -18,7 +18,23 @@ namespace BTCPayServer.Data
public DateTimeOffset Date { get; set; }
public string PullPaymentDataId { get; set; }
public string StoreDataId { get; set; }
/// <summary>
/// The currency of the payout (eg. BTC)
/// </summary>
public string Currency { get; set; }
/// <summary>
/// The amount of the payout in Currency.
/// The Amount only get set when the payout is actually approved.
/// </summary>
public decimal? Amount { get; set; }
/// <summary>
/// The original currency of the payout (eg. USD)
/// </summary>
public string OriginalCurrency { get; set; }
/// <summary>
/// The amount of the payout in OriginalCurrency
/// </summary>
public decimal OriginalAmount { get; set; }
public PullPaymentData PullPaymentData { get; set; }
[MaxLength(20)]
public PayoutState State { get; set; }
@ -28,7 +44,14 @@ namespace BTCPayServer.Data
public string Blob { get; set; }
public string Proof { get; set; }
#nullable enable
public string? Destination { get; set; }
/// <summary>
/// For example, BTC-CHAIN needs to ensure that only a single address is tied to an active payout.
/// If `PayoutBlob.Destination` is `bitcoin://1BvBMSeYstWetqTFn5Au4m4GFg7xJaNVN2?amount=0.1`
/// Then `DedupId` is `1BvBMSeYstWetqTFn5Au4m4GFg7xJaNVN2`
/// For Lightning, Destination could be the lightning address, BOLT11 or LNURL
/// But the `DedupId` would be the `PaymentHash`.
/// </summary>
public string? DedupId { get; set; }
#nullable restore
public StoreData StoreData { get; set; }
@ -46,7 +69,7 @@ namespace BTCPayServer.Data
builder.Entity<PayoutData>()
.HasIndex(o => o.State);
builder.Entity<PayoutData>()
.HasIndex(x => new { DestinationId = x.Destination, x.State });
.HasIndex(x => new { DestinationId = x.DedupId, x.State });
builder.Entity<PayoutData>()
.Property(o => o.Blob)

@ -16,7 +16,7 @@ public class PayoutProcessorData : IHasBlobUntyped
public string Id { get; set; }
public string StoreId { get; set; }
public StoreData Store { get; set; }
public string PaymentMethod { get; set; }
public string PayoutMethodId { get; set; }
public string Processor { get; set; }
[Obsolete("Use Blob2 instead")]
@ -36,6 +36,6 @@ public class PayoutProcessorData : IHasBlobUntyped
public override string ToString()
{
return $"{Processor} {PaymentMethod} {StoreId}";
return $"{Processor} {PayoutMethodId} {StoreId}";
}
}

@ -1,18 +0,0 @@
using Microsoft.EntityFrameworkCore;
namespace BTCPayServer.Data
{
public class PendingInvoiceData
{
public string Id { get; set; }
public InvoiceData InvoiceData { get; set; }
internal static void OnModelCreating(ModelBuilder builder)
{
builder.Entity<PendingInvoiceData>()
.HasOne(o => o.InvoiceData)
.WithMany(o => o.PendingInvoices)
.HasForeignKey(o => o.Id).OnDelete(DeleteBehavior.Cascade);
}
}
}

@ -24,6 +24,8 @@ namespace BTCPayServer.Data
public StoreData StoreData { get; set; }
[MaxLength(50)]
public string StoreId { get; set; }
public string Currency { get; set; }
public decimal Limit { get; set; }
public DateTimeOffset StartDate { get; set; }
public DateTimeOffset? EndDate { get; set; }
public bool Archived { get; set; }

@ -1,30 +0,0 @@
using System;
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace BTCPayServer.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20240405052858_cleanup_address_invoices")]
public partial class cleanup_address_invoices : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(@"
DELETE FROM ""AddressInvoices"" WHERE ""Address"" LIKE '%_LightningLike';
ALTER TABLE ""AddressInvoices"" DROP COLUMN IF EXISTS ""CreatedTime"";
");
migrationBuilder.Sql(@"VACUUM (FULL, ANALYZE) ""AddressInvoices"";", true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

@ -1,29 +0,0 @@
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace BTCPayServer.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20240520042729_payoutsmigration")]
public partial class payoutsmigration : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "Currency",
table: "Payouts",
type: "text",
nullable: true);
migrationBuilder.RenameColumn("PaymentMethodId", "Payouts", "PayoutMethodId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

File diff suppressed because it is too large Load Diff

@ -1,56 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace BTCPayServer.Migrations
{
/// <inheritdoc />
public partial class AppStuff : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "AppStorageItems",
columns: table => new
{
Key = table.Column<string>(type: "text", nullable: false),
Version = table.Column<long>(type: "bigint", nullable: false),
UserId = table.Column<string>(type: "text", nullable: false),
Value = table.Column<byte[]>(type: "bytea", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AppStorageItems", x => new { x.Key, x.Version, x.UserId });
table.ForeignKey(
name: "FK_AppStorageItems_AspNetUsers_UserId",
column: x => x.UserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_AppStorageItems_Key_UserId",
table: "AppStorageItems",
columns: new[] { "Key", "UserId" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_AppStorageItems_UserId",
table: "AppStorageItems",
column: "UserId");
migrationBuilder.Sql("CREATE FUNCTION \"LC_TRIGGER_BEFORE_INSERT_APPSTORAGEITEMDATA\"() RETURNS trigger as $LC_TRIGGER_BEFORE_INSERT_APPSTORAGEITEMDATA$\r\nBEGIN\r\n DELETE FROM \"AppStorageItems\"\r\n WHERE NEW.\"UserId\" = \"AppStorageItems\".\"UserId\" AND NEW.\"Key\" = \"AppStorageItems\".\"Key\" AND NEW.\"Version\" > \"AppStorageItems\".\"Version\";\r\nRETURN NEW;\r\nEND;\r\n$LC_TRIGGER_BEFORE_INSERT_APPSTORAGEITEMDATA$ LANGUAGE plpgsql;\r\nCREATE TRIGGER LC_TRIGGER_BEFORE_INSERT_APPSTORAGEITEMDATA BEFORE INSERT\r\nON \"AppStorageItems\"\r\nFOR EACH ROW EXECUTE PROCEDURE \"LC_TRIGGER_BEFORE_INSERT_APPSTORAGEITEMDATA\"();");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("DROP FUNCTION \"LC_TRIGGER_BEFORE_INSERT_APPSTORAGEITEMDATA\"() CASCADE;");
migrationBuilder.DropTable(
name: "AppStorageItems");
}
}
}

@ -0,0 +1,35 @@
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace BTCPayServer.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20240826065950_removeinvoicecols")]
[DBScript("001.InvoiceFunctions.sql")]
public partial class removeinvoicecols : DBScriptsMigration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_Invoices_OrderId",
table: "Invoices");
migrationBuilder.DropColumn(
name: "ItemCode",
table: "Invoices");
migrationBuilder.DropColumn(
name: "OrderId",
table: "Invoices");
migrationBuilder.DropColumn(
name: "CustomerEmail",
table: "Invoices");
base.Up(migrationBuilder);
}
}
}

@ -0,0 +1,40 @@
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace BTCPayServer.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20240827034505_migratepayouts")]
[DBScript("002.RefactorPayouts.sql")]
public partial class migratepayouts : DBScriptsMigration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
base.Up(migrationBuilder);
migrationBuilder.RenameColumn(
name: "Destination",
table: "Payouts",
newName: "DedupId");
migrationBuilder.RenameIndex(
name: "IX_Payouts_Destination_State",
table: "Payouts",
newName: "IX_Payouts_DedupId_State");
migrationBuilder.RenameColumn(
name: "PaymentMethod",
table: "PayoutProcessors",
newName: "PayoutMethodId");
migrationBuilder.Sql("""
UPDATE "PayoutProcessors"
SET
"PayoutMethodId" = CASE WHEN STRPOS("PayoutMethodId", '_') = 0 THEN "PayoutMethodId" || '-CHAIN'
WHEN STRPOS("PayoutMethodId", '_LightningLike') > 0 THEN split_part("PayoutMethodId", '_LightningLike', 1) || '-LN'
WHEN STRPOS("PayoutMethodId", '_LNURLPAY') > 0 THEN split_part("PayoutMethodId",'_LNURLPAY', 1) || '-LN'
ELSE "PayoutMethodId" END
""");
}
}
}

@ -0,0 +1,54 @@
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Newtonsoft.Json;
#nullable disable
namespace BTCPayServer.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20240904092905_UpdateStoreOwnerRole")]
public partial class UpdateStoreOwnerRole : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.UpdateData(
"StoreRoles",
keyColumns: new[] { "Id" },
keyColumnTypes: new[] { "TEXT" },
keyValues: new[] { "Owner" },
columns: new[] { "Permissions" },
columnTypes: new[] { "TEXT[]" },
values: new object[]
{
new[]
{
"btcpay.store.canmodifystoresettings"
}
});
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.UpdateData(
"StoreRoles",
keyColumns: new[] { "Id" },
keyColumnTypes: new[] { "TEXT" },
keyValues: new[] { "Owner" },
columns: new[] { "Permissions" },
columnTypes: new[] { "TEXT[]" },
values: new object[]
{
new[]
{
"btcpay.store.canmodifystoresettings",
"btcpay.store.cantradecustodianaccount",
"btcpay.store.canwithdrawfromcustodianaccount",
"btcpay.store.candeposittocustodianaccount"
}
});
}
}
}

@ -0,0 +1,15 @@
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace BTCPayServer.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20240913034505_refactorpendinginvoicespayments")]
[DBScript("003.RefactorPendingInvoicesPayments.sql")]
public partial class refactorpendinginvoicespayments : DBScriptsMigration
{
}
}

@ -0,0 +1,80 @@
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace BTCPayServer.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20240919085726_refactorinvoiceaddress")]
public partial class refactorinvoiceaddress : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropPrimaryKey(
name: "PK_AddressInvoices",
table: "AddressInvoices");
migrationBuilder.AddColumn<string>(
name: "PaymentMethodId",
table: "AddressInvoices",
type: "text",
nullable: false,
defaultValue: "");
migrationBuilder.Sql("""
UPDATE "AddressInvoices"
SET
"Address" = (string_to_array("Address", '#'))[1],
"PaymentMethodId" = CASE WHEN (string_to_array("Address", '#'))[2] IS NULL THEN 'BTC-CHAIN'
WHEN STRPOS((string_to_array("Address", '#'))[2], '_') = 0 THEN (string_to_array("Address", '#'))[2] || '-CHAIN'
WHEN STRPOS((string_to_array("Address", '#'))[2], '_MoneroLike') > 0 THEN replace((string_to_array("Address", '#'))[2],'_MoneroLike','-CHAIN')
WHEN STRPOS((string_to_array("Address", '#'))[2], '_ZcashLike') > 0 THEN replace((string_to_array("Address", '#'))[2],'_ZcashLike','-CHAIN')
ELSE '' END;
ALTER TABLE "AddressInvoices" DROP COLUMN IF EXISTS "CreatedTime";
DELETE FROM "AddressInvoices" WHERE "PaymentMethodId" = '';
""");
migrationBuilder.AddPrimaryKey(
name: "PK_AddressInvoices",
table: "AddressInvoices",
columns: new[] { "Address", "PaymentMethodId" });
migrationBuilder.Sql("VACUUM (ANALYZE) \"AddressInvoices\";", true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropPrimaryKey(
name: "PK_AddressInvoices",
table: "AddressInvoices");
migrationBuilder.DropColumn(
name: "PaymentMethodId",
table: "AddressInvoices");
migrationBuilder.AddPrimaryKey(
name: "PK_AddressInvoices",
table: "AddressInvoices",
column: "Address");
migrationBuilder.CreateTable(
name: "PendingInvoices",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_PendingInvoices", x => x.Id);
table.ForeignKey(
name: "FK_PendingInvoices_Invoices_Id",
column: x => x.Id,
principalTable: "Invoices",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
}
}
}

@ -0,0 +1,38 @@
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace BTCPayServer.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20240923065254_refactorpayments")]
public partial class refactorpayments : DBScriptsMigration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropPrimaryKey(
name: "PK_Payments",
table: "Payments");
migrationBuilder.RenameColumn(
name: "Type",
table: "Payments",
newName: "PaymentMethodId");
migrationBuilder.Sql("UPDATE \"Payments\" SET \"PaymentMethodId\"='' WHERE \"PaymentMethodId\" IS NULL;");
migrationBuilder.AddPrimaryKey(
name: "PK_Payments",
table: "Payments",
columns: new[] { "Id", "PaymentMethodId" });
base.Up(migrationBuilder);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

@ -0,0 +1,15 @@
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace BTCPayServer.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20240924065254_monitoredinvoices")]
[DBScript("004.MonitoredInvoices.sql")]
public partial class monitoredinvoices : DBScriptsMigration
{
}
}

@ -23,35 +23,6 @@ namespace BTCPayServer.Migrations
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("BTCPayServer.App.BackupStorage.AppStorageItemData", b =>
{
b.Property<string>("Key")
.HasColumnType("text");
b.Property<long>("Version")
.HasColumnType("bigint");
b.Property<string>("UserId")
.HasColumnType("text");
b.Property<byte[]>("Value")
.HasColumnType("bytea");
b.HasKey("Key", "Version", "UserId");
b.HasIndex("UserId");
b.HasIndex("Key", "UserId")
.IsUnique();
b.ToTable("AppStorageItems", t =>
{
t.HasTrigger("LC_TRIGGER_BEFORE_INSERT_APPSTORAGEITEMDATA");
});
b.HasAnnotation("LC_TRIGGER_BEFORE_INSERT_APPSTORAGEITEMDATA", "CREATE FUNCTION \"LC_TRIGGER_BEFORE_INSERT_APPSTORAGEITEMDATA\"() RETURNS trigger as $LC_TRIGGER_BEFORE_INSERT_APPSTORAGEITEMDATA$\r\nBEGIN\r\n DELETE FROM \"AppStorageItems\"\r\n WHERE NEW.\"UserId\" = \"AppStorageItems\".\"UserId\" AND NEW.\"Key\" = \"AppStorageItems\".\"Key\" AND NEW.\"Version\" > \"AppStorageItems\".\"Version\";\r\nRETURN NEW;\r\nEND;\r\n$LC_TRIGGER_BEFORE_INSERT_APPSTORAGEITEMDATA$ LANGUAGE plpgsql;\r\nCREATE TRIGGER LC_TRIGGER_BEFORE_INSERT_APPSTORAGEITEMDATA BEFORE INSERT\r\nON \"AppStorageItems\"\r\nFOR EACH ROW EXECUTE PROCEDURE \"LC_TRIGGER_BEFORE_INSERT_APPSTORAGEITEMDATA\"();");
});
modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b =>
{
b.Property<string>("Id")
@ -92,10 +63,13 @@ namespace BTCPayServer.Migrations
b.Property<string>("Address")
.HasColumnType("text");
b.Property<string>("PaymentMethodId")
.HasColumnType("text");
b.Property<string>("InvoiceDataId")
.HasColumnType("text");
b.HasKey("Address");
b.HasKey("Address", "PaymentMethodId");
b.HasIndex("InvoiceDataId");
@ -296,18 +270,9 @@ namespace BTCPayServer.Migrations
b.Property<string>("Currency")
.HasColumnType("text");
b.Property<string>("CustomerEmail")
.HasColumnType("text");
b.Property<string>("ExceptionStatus")
.HasColumnType("text");
b.Property<string>("ItemCode")
.HasColumnType("text");
b.Property<string>("OrderId")
.HasColumnType("text");
b.Property<string>("Status")
.HasColumnType("text");
@ -324,8 +289,6 @@ namespace BTCPayServer.Migrations
b.HasIndex("Created");
b.HasIndex("OrderId");
b.HasIndex("StoreDataId");
b.ToTable("Invoices");
@ -519,6 +482,9 @@ namespace BTCPayServer.Migrations
b.Property<string>("Id")
.HasColumnType("text");
b.Property<string>("PaymentMethodId")
.HasColumnType("text");
b.Property<bool?>("Accounted")
.HasColumnType("boolean");
@ -543,10 +509,7 @@ namespace BTCPayServer.Migrations
b.Property<string>("Status")
.HasColumnType("text");
b.Property<string>("Type")
.HasColumnType("text");
b.HasKey("Id");
b.HasKey("Id", "PaymentMethodId");
b.HasIndex("InvoiceDataId");
@ -593,6 +556,9 @@ namespace BTCPayServer.Migrations
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<decimal?>("Amount")
.HasColumnType("numeric");
b.Property<string>("Blob")
.HasColumnType("JSONB");
@ -602,7 +568,13 @@ namespace BTCPayServer.Migrations
b.Property<DateTimeOffset>("Date")
.HasColumnType("timestamp with time zone");
b.Property<string>("Destination")
b.Property<string>("DedupId")
.HasColumnType("text");
b.Property<decimal>("OriginalAmount")
.HasColumnType("numeric");
b.Property<string>("OriginalCurrency")
.HasColumnType("text");
b.Property<string>("PayoutMethodId")
@ -632,7 +604,7 @@ namespace BTCPayServer.Migrations
b.HasIndex("StoreDataId");
b.HasIndex("Destination", "State");
b.HasIndex("DedupId", "State");
b.ToTable("Payouts");
});
@ -649,7 +621,7 @@ namespace BTCPayServer.Migrations
b.Property<string>("Blob2")
.HasColumnType("JSONB");
b.Property<string>("PaymentMethod")
b.Property<string>("PayoutMethodId")
.HasColumnType("text");
b.Property<string>("Processor")
@ -665,16 +637,6 @@ namespace BTCPayServer.Migrations
b.ToTable("PayoutProcessors");
});
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("PendingInvoices");
});
modelBuilder.Entity("BTCPayServer.Data.PlannedTransaction", b =>
{
b.Property<string>("Id")
@ -704,9 +666,15 @@ namespace BTCPayServer.Migrations
b.Property<string>("Blob")
.HasColumnType("JSONB");
b.Property<string>("Currency")
.HasColumnType("text");
b.Property<DateTimeOffset?>("EndDate")
.HasColumnType("timestamp with time zone");
b.Property<decimal>("Limit")
.HasColumnType("numeric");
b.Property<DateTimeOffset>("StartDate")
.HasColumnType("timestamp with time zone");
@ -1181,17 +1149,6 @@ namespace BTCPayServer.Migrations
b.ToTable("AspNetUserTokens", (string)null);
});
modelBuilder.Entity("BTCPayServer.App.BackupStorage.AppStorageItemData", b =>
{
b.HasOne("BTCPayServer.Data.ApplicationUser", "User")
.WithMany("AppStorageItems")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
@ -1367,17 +1324,6 @@ namespace BTCPayServer.Migrations
b.Navigation("Store");
});
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("PendingInvoices")
.HasForeignKey("Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("InvoiceData");
});
modelBuilder.Entity("BTCPayServer.Data.PullPaymentData", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
@ -1587,8 +1533,6 @@ namespace BTCPayServer.Migrations
{
b.Navigation("APIKeys");
b.Navigation("AppStorageItems");
b.Navigation("Fido2Credentials");
b.Navigation("Notifications");
@ -1610,8 +1554,6 @@ namespace BTCPayServer.Migrations
b.Navigation("Payments");
b.Navigation("PendingInvoices");
b.Navigation("Refunds");
});

@ -34,16 +34,13 @@
<PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.15" />
<PackageReference Include="Selenium.Support" Version="4.1.1" />
<PackageReference Include="Selenium.WebDriver" Version="4.22.0" />
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="125.0.6422.14100" />
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="128.0.6613.11900" />
<PackageReference Include="xunit" Version="2.6.6" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.6">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup Condition="'$(Altcoins)' != 'true'">
<Compile Remove="AltcoinTests\**\*.cs"></Compile>
</ItemGroup>
<ItemGroup>
<None Update=".dockerignore">
<DependentUpon>Dockerfile</DependentUpon>

@ -31,12 +31,6 @@ using AuthenticationSchemes = BTCPayServer.Abstractions.Constants.Authentication
namespace BTCPayServer.Tests
{
public enum TestDatabases
{
Postgres,
MySQL,
}
public class BTCPayServerTester : IDisposable
{
internal readonly string _Directory;
@ -69,11 +63,6 @@ namespace BTCPayServer.Tests
set;
}
public string MySQL
{
get; set;
}
public string Postgres
{
get; set;
@ -89,11 +78,6 @@ namespace BTCPayServer.Tests
get; set;
}
public TestDatabases TestDatabase
{
get; set;
}
public async Task RestartStartupTask<T>()
{
var startupTask = GetService<IServiceProvider>().GetServices<Abstractions.Contracts.IStartupTask>()
@ -164,9 +148,7 @@ namespace BTCPayServer.Tests
if (!string.IsNullOrEmpty(SSHConnection))
config.AppendLine($"sshconnection={SSHConnection}");
if (TestDatabase == TestDatabases.MySQL && !String.IsNullOrEmpty(MySQL))
config.AppendLine($"mysql=" + MySQL);
else if (!String.IsNullOrEmpty(Postgres))
if (!String.IsNullOrEmpty(Postgres))
config.AppendLine($"postgres=" + Postgres);
if (!string.IsNullOrEmpty(ExplorerPostgres))
@ -195,7 +177,7 @@ namespace BTCPayServer.Tests
l.AddFilter("System.Net.Http.HttpClient", LogLevel.Critical);
l.SetMinimumLevel(LogLevel.Information)
.AddFilter("Microsoft", LogLevel.Error)
.AddFilter("Hangfire", LogLevel.Error)
.AddFilter("Microsoft.EntityFrameworkCore.Migrations", LogLevel.Information)
.AddFilter("Fido2NetLib.DistributedCacheMetadataService", LogLevel.Error)
.AddProvider(LoggerProvider);
});

@ -58,8 +58,8 @@ namespace BTCPayServer.Tests
var qrValue = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-qr-value");
var clipboard = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
var payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href");
var address = s.Driver.FindElement(By.CssSelector("#Address_BTC-CHAIN .truncate-center-start")).Text;
Assert.StartsWith("bcrt", s.Driver.FindElement(By.CssSelector("#Address_BTC-CHAIN .truncate-center-start")).Text);
var address = s.Driver.FindElement(By.CssSelector("#Address_BTC-CHAIN .truncate-center")).GetAttribute("data-text");
Assert.StartsWith("bcrt", address);
Assert.DoesNotContain("lightning=", payUrl);
Assert.Equal($"bitcoin:{address}", payUrl);
Assert.Equal($"bitcoin:{address}", clipboard);
@ -80,7 +80,7 @@ namespace BTCPayServer.Tests
{
payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href");
Assert.StartsWith("lightning:lnurl", payUrl);
Assert.StartsWith("lnurl", s.Driver.WaitForElement(By.CssSelector("#Lightning_BTC-CHAIN .truncate-center-start")).Text);
Assert.StartsWith("lnurl", s.Driver.WaitForElement(By.CssSelector("#Lightning_BTC-CHAIN .truncate-center")).GetAttribute("data-text"));
s.Driver.ElementDoesNotExist(By.Id("Address_BTC-CHAIN"));
});
@ -94,7 +94,7 @@ namespace BTCPayServer.Tests
qrValue = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-qr-value");
clipboard = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href");
address = s.Driver.FindElement(By.CssSelector("#Lightning_BTC-LN .truncate-center-start")).Text;
address = s.Driver.FindElement(By.CssSelector("#Lightning_BTC-LN .truncate-center")).GetAttribute("data-text");
Assert.Equal($"lightning:{address}", payUrl);
Assert.Equal($"lightning:{address}", clipboard);
Assert.Equal($"lightning:{address.ToUpperInvariant()}", qrValue);
@ -147,7 +147,7 @@ namespace BTCPayServer.Tests
invoiceId = s.CreateInvoice(2100, "EUR");
s.GoToInvoiceCheckout(invoiceId);
await Task.Delay(200);
address = s.Driver.FindElement(By.CssSelector("#Address_BTC-CHAIN .truncate-center-start")).Text;
address = s.Driver.FindElement(By.CssSelector("#Address_BTC-CHAIN .truncate-center")).GetAttribute("data-text");
var amountFraction = "0.00001";
await s.Server.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create(address, Network.RegTest),
Money.Parse(amountFraction));
@ -261,8 +261,8 @@ namespace BTCPayServer.Tests
qrValue = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-qr-value");
clipboard = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href");
var copyAddressOnchain = s.Driver.FindElement(By.CssSelector("#Address_BTC-CHAIN .truncate-center-start")).Text;
var copyAddressLightning = s.Driver.FindElement(By.CssSelector("#Lightning_BTC-CHAIN .truncate-center-start")).Text;
var copyAddressOnchain = s.Driver.FindElement(By.CssSelector("#Address_BTC-CHAIN .truncate-center")).GetAttribute("data-text");
var copyAddressLightning = s.Driver.FindElement(By.CssSelector("#Lightning_BTC-CHAIN .truncate-center")).GetAttribute("data-text");
Assert.StartsWith($"bitcoin:{copyAddressOnchain}?amount=", payUrl);
Assert.Contains("?amount=", payUrl);
Assert.Contains("&lightning=", payUrl);
@ -327,8 +327,8 @@ namespace BTCPayServer.Tests
qrValue = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-qr-value");
clipboard = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href");
copyAddressOnchain = s.Driver.FindElement(By.CssSelector("#Address_BTC-CHAIN .truncate-center-start")).Text;
copyAddressLightning = s.Driver.FindElement(By.CssSelector("#Lightning_BTC-CHAIN .truncate-center-start")).Text;
copyAddressOnchain = s.Driver.FindElement(By.CssSelector("#Address_BTC-CHAIN .truncate-center")).GetAttribute("data-text");
copyAddressLightning = s.Driver.FindElement(By.CssSelector("#Lightning_BTC-CHAIN .truncate-center")).GetAttribute("data-text");
Assert.StartsWith($"bitcoin:{copyAddressOnchain}", payUrl);
Assert.Contains("?lightning=lnurl", payUrl);
Assert.DoesNotContain("amount=", payUrl);

@ -39,6 +39,8 @@ namespace BTCPayServer.Tests
await user.GrantAccessAsync();
var user2 = tester.NewAccount();
await user2.GrantAccessAsync();
await user.RegisterDerivationSchemeAsync("BTC");
await user2.RegisterDerivationSchemeAsync("BTC");
var apps = user.GetController<UIAppsController>();
var apps2 = user2.GetController<UIAppsController>();
var crowdfund = user.GetController<UICrowdfundController>();

@ -0,0 +1,101 @@
using System;
using System.Data;
using System.Data.Common;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Data;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Tests.Logging;
using Dapper;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NBitcoin;
using Npgsql;
namespace BTCPayServer.Tests
{
public class DatabaseTester
{
private readonly ILoggerFactory _loggerFactory;
private readonly string dbname;
private string[] notAppliedMigrations;
public DatabaseTester(ILog log, ILoggerFactory loggerFactory)
{
var connStr = Environment.GetEnvironmentVariable("TESTS_POSTGRES");
if (string.IsNullOrEmpty(connStr))
connStr = ServerTester.DefaultConnectionString;
var r = RandomUtils.GetUInt32();
dbname = $"btcpayserver{r}";
connStr = connStr.Replace("btcpayserver", dbname);
log.LogInformation("DB: " + dbname);
ConnectionString = connStr;
_loggerFactory = loggerFactory;
}
public ApplicationDbContextFactory CreateContextFactory()
{
return new ApplicationDbContextFactory(new OptionsWrapper<DatabaseOptions>(new DatabaseOptions()
{
ConnectionString = ConnectionString
}), _loggerFactory);
}
public InvoiceRepository GetInvoiceRepository()
{
var logs = new BTCPayServer.Logging.Logs();
logs.Configure(_loggerFactory);
return new InvoiceRepository(CreateContextFactory(), new EventAggregator(logs));
}
public ApplicationDbContext CreateContext() => CreateContextFactory().CreateContext();
public async Task MigrateAsync()
{
using var ctx = CreateContext();
await EnsureCreatedAsync();
await ctx.Database.MigrateAsync();
}
private async Task EnsureCreatedAsync()
{
var builder = new Npgsql.NpgsqlConnectionStringBuilder(ConnectionString);
builder.Database = null;
NpgsqlConnection conn = new NpgsqlConnection(builder.ToString());
await conn.ExecuteAsync($"CREATE DATABASE \"{dbname}\";");
}
public async Task MigrateUntil(string migration = null)
{
using var ctx = CreateContext();
var db = ctx.Database.GetDbConnection();
await EnsureCreatedAsync();
var migrations = ctx.Database.GetMigrations().ToArray();
if (migration is not null)
{
var untilMigrationIdx = Array.IndexOf(migrations, migration);
if (untilMigrationIdx == -1)
throw new InvalidOperationException($"Migration {migration} not found");
notAppliedMigrations = migrations[untilMigrationIdx..];
await db.ExecuteAsync("CREATE TABLE IF NOT EXISTS \"__EFMigrationsHistory\" (\"MigrationId\" TEXT, \"ProductVersion\" TEXT)");
await db.ExecuteAsync("INSERT INTO \"__EFMigrationsHistory\" VALUES (@migration, '8.0.0')", notAppliedMigrations.Select(m => new { migration = m }).ToArray());
}
await ctx.Database.MigrateAsync();
}
public async Task ContinueMigration()
{
if (notAppliedMigrations is null)
throw new InvalidOperationException("Call MigrateUpTo first");
using var ctx = CreateContext();
var db = ctx.Database.GetDbConnection();
await db.ExecuteAsync("DELETE FROM \"__EFMigrationsHistory\" WHERE \"MigrationId\" = ANY (@migrations)", new { migrations = notAppliedMigrations });
await ctx.Database.MigrateAsync();
notAppliedMigrations = null;
}
public string ConnectionString { get; }
}
}

@ -0,0 +1,217 @@
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Payments;
using Dapper;
using Microsoft.EntityFrameworkCore;
using NBitcoin;
using NBitcoin.Altcoins;
using NBitpayClient;
using Newtonsoft.Json.Linq;
using Xunit;
using Xunit.Abstractions;
namespace BTCPayServer.Tests
{
[Trait("Integration", "Integration")]
public class DatabaseTests : UnitTestBase
{
public DatabaseTests(ITestOutputHelper helper):base(helper)
{
}
[Fact]
public async Task CanQueryMonitoredInvoices()
{
var tester = CreateDBTester();
await tester.MigrateUntil();
var invoiceRepository = tester.GetInvoiceRepository();
using var ctx = tester.CreateContext();
var conn = ctx.Database.GetDbConnection();
async Task AddPrompt(string invoiceId, string paymentMethodId, bool activated = true)
{
JObject prompt = new JObject();
if (!activated)
prompt["inactive"] = true;
prompt["currency"] = "USD";
var query = """
UPDATE "Invoices" SET "Blob2" = jsonb_set('{"prompts": {}}'::JSONB || COALESCE("Blob2",'{}'), ARRAY['prompts','@paymentMethodId'], '@prompt'::JSONB)
WHERE "Id" = '@invoiceId'
""";
query = query.Replace("@paymentMethodId", paymentMethodId);
query = query.Replace("@prompt", prompt.ToString());
query = query.Replace("@invoiceId", invoiceId);
Assert.Equal(1, await conn.ExecuteAsync(query));
}
await conn.ExecuteAsync("""
INSERT INTO "Invoices" ("Id", "Created", "Status","Currency") VALUES
('BTCOnly', NOW(), 'New', 'USD'),
('LTCOnly', NOW(), 'New', 'USD'),
('LTCAndBTC', NOW(), 'New', 'USD'),
('LTCAndBTCLazy', NOW(), 'New', 'USD')
""");
foreach (var invoiceId in new string[] { "LTCOnly", "LTCAndBTCLazy", "LTCAndBTC" })
{
await AddPrompt(invoiceId, "LTC-CHAIN", true);
}
foreach (var invoiceId in new string[] { "BTCOnly", "LTCAndBTC" })
{
await AddPrompt(invoiceId, "BTC-CHAIN", true);
}
await AddPrompt("LTCAndBTCLazy", "BTC-CHAIN", false);
var btc = PaymentMethodId.Parse("BTC-CHAIN");
var ltc = PaymentMethodId.Parse("LTC-CHAIN");
var invoices = await invoiceRepository.GetMonitoredInvoices(btc);
Assert.Equal(2, invoices.Length);
foreach (var invoiceId in new[] { "BTCOnly", "LTCAndBTC" })
{
Assert.Contains(invoices, i => i.Id == invoiceId);
}
invoices = await invoiceRepository.GetMonitoredInvoices(btc, true);
Assert.Equal(3, invoices.Length);
foreach (var invoiceId in new[] { "BTCOnly", "LTCAndBTC", "LTCAndBTCLazy" })
{
Assert.Contains(invoices, i => i.Id == invoiceId);
}
invoices = await invoiceRepository.GetMonitoredInvoices(ltc);
Assert.Equal(3, invoices.Length);
foreach (var invoiceId in new[] { "LTCAndBTC", "LTCAndBTC", "LTCAndBTCLazy" })
{
Assert.Contains(invoices, i => i.Id == invoiceId);
}
await conn.ExecuteAsync("""
INSERT INTO "Payments" ("Id", "InvoiceDataId", "PaymentMethodId", "Status", "Blob2", "Created", "Amount", "Currency") VALUES
('1','LTCAndBTC', 'LTC-CHAIN', 'Processing', '{}'::JSONB, NOW(), 123, 'USD'),
('2','LTCAndBTC', 'BTC-CHAIN', 'Processing', '{}'::JSONB, NOW(), 123, 'USD'),
('3','LTCAndBTC', 'BTC-CHAIN', 'Processing', '{}'::JSONB, NOW(), 123, 'USD'),
('4','LTCAndBTC', 'BTC-CHAIN', 'Settled', '{}'::JSONB, NOW(), 123, 'USD');
INSERT INTO "AddressInvoices" ("InvoiceDataId", "Address", "PaymentMethodId") VALUES
('LTCAndBTC', 'BTC1', 'BTC-CHAIN'),
('LTCAndBTC', 'BTC2', 'BTC-CHAIN'),
('LTCAndBTC', 'LTC1', 'LTC-CHAIN');
""");
var invoice = Assert.Single(await invoiceRepository.GetMonitoredInvoices(ltc), i => i.Id == "LTCAndBTC");
var payment = Assert.Single(invoice.GetPayments(false));
Assert.Equal("1", payment.Id);
foreach (var includeNonActivated in new[] { true, false })
{
invoices = await invoiceRepository.GetMonitoredInvoices(btc, includeNonActivated);
invoice = Assert.Single(invoices, i => i.Id == "LTCAndBTC");
var payments = invoice.GetPayments(false);
Assert.Equal(3, payments.Count);
foreach (var paymentId in new[] { "2", "3", "4" })
{
Assert.Contains(payments, p => p.Id == paymentId);
}
Assert.Equal(2, invoice.Addresses.Count);
foreach (var addr in new[] { "BTC1", "BTC2" })
{
Assert.Contains(invoice.Addresses, p => p.Address == addr);
}
if (!includeNonActivated)
Assert.DoesNotContain(invoices, i => i.Id == "LTCAndBTCLazy");
else
Assert.Contains(invoices, i => i.Id == "LTCAndBTCLazy");
}
}
[Fact]
public async Task CanMigrateInvoiceAddresses()
{
var tester = CreateDBTester();
await tester.MigrateUntil("20240919085726_refactorinvoiceaddress");
using var ctx = tester.CreateContext();
var conn = ctx.Database.GetDbConnection();
await conn.ExecuteAsync("INSERT INTO \"Invoices\" (\"Id\", \"Created\") VALUES ('i', NOW())");
await conn.ExecuteAsync(
"INSERT INTO \"AddressInvoices\" VALUES ('aaa#BTC', 'i'),('bbb','i'),('ccc#BTC_LNU', 'i'),('ddd#XMR_MoneroLike', 'i'),('eee#ZEC_ZcashLike', 'i')");
await tester.ContinueMigration();
foreach (var v in new[] { ("aaa", "BTC-CHAIN"), ("bbb", "BTC-CHAIN"), ("ddd", "XMR-CHAIN") , ("eee", "ZEC-CHAIN") })
{
var ok = await conn.ExecuteScalarAsync<bool>("SELECT 't'::BOOLEAN FROM \"AddressInvoices\" WHERE \"Address\"=@a AND \"PaymentMethodId\"=@b", new { a = v.Item1, b = v.Item2 });
Assert.True(ok);
}
var notok = await conn.ExecuteScalarAsync<bool>("SELECT 't'::BOOLEAN FROM \"AddressInvoices\" WHERE \"Address\"='ccc'");
Assert.False(notok);
}
[Fact]
public async Task CanMigratePayoutsAndPullPayments()
{
var tester = CreateDBTester();
await tester.MigrateUntil("20240827034505_migratepayouts");
using var ctx = tester.CreateContext();
var conn = ctx.Database.GetDbConnection();
await conn.ExecuteAsync("INSERT INTO \"Stores\"(\"Id\", \"SpeedPolicy\") VALUES (@store, 0)", new { store = "store1" });
var param = new
{
Id = "pp1",
StoreId = "store1",
Blob = "{\"Name\": \"CoinLottery\", \"View\": {\"Email\": null, \"Title\": \"\", \"Description\": \"\", \"EmbeddedCSS\": null, \"CustomCSSLink\": null}, \"Limit\": \"10.00\", \"Period\": null, \"Currency\": \"GBP\", \"Description\": \"\", \"Divisibility\": 0, \"MinimumClaim\": \"0\", \"AutoApproveClaims\": false, \"SupportedPaymentMethods\": [\"BTC\", \"BTC_LightningLike\"]}"
};
await conn.ExecuteAsync("INSERT INTO \"PullPayments\"(\"Id\", \"StoreId\", \"Blob\", \"StartDate\", \"Archived\") VALUES (@Id, @StoreId, @Blob::JSONB, NOW(), 'f')", param);
var parameters = new[]
{
new
{
Id = "p1",
StoreId = "store1",
PullPaymentDataId = "pp1",
PaymentMethodId = "BTC",
Blob = "{\"Amount\": \"10.0\", \"Revision\": 0, \"Destination\": \"address\", \"CryptoAmount\": \"0.00012225\", \"MinimumConfirmation\": 1}"
},
new
{
Id = "p2",
StoreId = "store1",
PullPaymentDataId = "pp1",
PaymentMethodId = "BTC_LightningLike",
Blob = "{\"Amount\": \"10.0\", \"Revision\": 0, \"Destination\": \"address\", \"CryptoAmount\": null, \"MinimumConfirmation\": 1}"
},
new
{
Id = "p3",
StoreId = "store1",
PullPaymentDataId = null as string,
PaymentMethodId = "BTC_LightningLike",
Blob = "{\"Amount\": \"10.0\", \"Revision\": 0, \"Destination\": \"address\", \"CryptoAmount\": null, \"MinimumConfirmation\": 1}"
},
new
{
Id = "p4",
StoreId = "store1",
PullPaymentDataId = null as string,
PaymentMethodId = "BTC_LightningLike",
Blob = "{\"Amount\": \"-10.0\", \"Revision\": 0, \"Destination\": \"address\", \"CryptoAmount\": null, \"MinimumConfirmation\": 1}"
}
};
await conn.ExecuteAsync("INSERT INTO \"Payouts\"(\"Id\", \"StoreDataId\", \"PullPaymentDataId\", \"PaymentMethodId\", \"Blob\", \"State\", \"Date\") VALUES (@Id, @StoreId, @PullPaymentDataId, @PaymentMethodId, @Blob::JSONB, 'state', NOW())", parameters);
await tester.ContinueMigration();
var migrated = await conn.ExecuteScalarAsync<bool>("SELECT 't'::BOOLEAN FROM \"PullPayments\" WHERE \"Id\"='pp1' AND \"Limit\"=10.0 AND \"Currency\"='GBP' AND \"Blob\"->>'SupportedPayoutMethods'='[\"BTC-CHAIN\", \"BTC-LN\"]'");
Assert.True(migrated);
migrated = await conn.ExecuteScalarAsync<bool>("SELECT 't'::BOOLEAN FROM \"Payouts\" WHERE \"Id\"='p1' AND \"Amount\"= 0.00012225 AND \"OriginalAmount\"=10.0 AND \"OriginalCurrency\"='GBP' AND \"PayoutMethodId\"='BTC-CHAIN'");
Assert.True(migrated);
migrated = await conn.ExecuteScalarAsync<bool>("SELECT 't'::BOOLEAN FROM \"Payouts\" WHERE \"Id\"='p2' AND \"Amount\" IS NULL AND \"OriginalAmount\"=10.0 AND \"OriginalCurrency\"='GBP' AND \"PayoutMethodId\"='BTC-LN'");
Assert.True(migrated);
migrated = await conn.ExecuteScalarAsync<bool>("SELECT 't'::BOOLEAN FROM \"Payouts\" WHERE \"Id\"='p3' AND \"Amount\" IS NULL AND \"OriginalAmount\"=10.0 AND \"OriginalCurrency\"='BTC'");
Assert.True(migrated);
migrated = await conn.ExecuteScalarAsync<bool>("SELECT 't'::BOOLEAN FROM \"Payouts\" WHERE \"Id\"='p4' AND \"Amount\" IS NULL AND \"OriginalAmount\"=-10.0 AND \"OriginalCurrency\"='BTC' AND \"PayoutMethodId\"='TOPUP'");
Assert.True(migrated);
}
}
}

@ -3,14 +3,15 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection.Metadata;
using System.Runtime.CompilerServices;
using System.Security;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Converters;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Configuration;
@ -19,21 +20,31 @@ using BTCPayServer.Data;
using BTCPayServer.HostedServices;
using BTCPayServer.Hosting;
using BTCPayServer.JsonConverters;
using BTCPayServer.Lightning;
using BTCPayServer.Logging;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Plugins;
using BTCPayServer.Plugins.Bitcoin;
using BTCPayServer.Rating;
using BTCPayServer.Services;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Fees;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Labels;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using BTCPayServer.Validation;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.Memory;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using NBitcoin;
using NBitcoin.DataEncoders;
using NBitcoin.Scripting.Parser;
using NBXplorer;
using NBXplorer.DerivationStrategy;
using NBXplorer.Models;
using Newtonsoft.Json;
@ -41,6 +52,7 @@ using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
using Xunit;
using Xunit.Abstractions;
using StoreData = BTCPayServer.Data.StoreData;
namespace BTCPayServer.Tests
{
@ -239,12 +251,10 @@ namespace BTCPayServer.Tests
Assert.Equal(id, id1);
Assert.Equal(id, id2);
Assert.Equal("LTC-LN", id.ToString());
#if ALTCOINS
id = PaymentMethodId.Parse("XMR");
id1 = PaymentMethodId.Parse("XMR-MoneroLike");
Assert.Equal(id, id1);
Assert.Equal("XMR-CHAIN", id.ToString());
#endif
}
[Fact]
@ -476,7 +486,7 @@ namespace BTCPayServer.Tests
#pragma warning restore CS0618
}
#if ALTCOINS
[Fact]
public void CanCalculateCryptoDue()
{
@ -496,7 +506,6 @@ namespace BTCPayServer.Tests
var paymentMethod = entity.GetPaymentPrompts().TryGet(PaymentTypes.CHAIN.GetPaymentMethodId("BTC"));
var accounting = paymentMethod.Calculate();
Assert.Equal(1.0m, accounting.ToSmallestUnit(Money.Satoshis(1.0m).ToDecimal(MoneyUnit.BTC)));
Assert.Equal(1.1m, accounting.Due);
Assert.Equal(1.1m, accounting.TotalDue);
@ -647,7 +656,6 @@ namespace BTCPayServer.Tests
Assert.Equal(accounting.Paid, accounting.TotalDue);
#pragma warning restore CS0618
}
#endif
[Fact]
public void DeterministicUTXOSorter()
@ -1617,7 +1625,7 @@ bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku
testCases.ForEach(tuple =>
{
Assert.Equal(tuple.expectedOutput, PosDataParser.ParsePosData(string.IsNullOrEmpty(tuple.input) ? null : JToken.Parse(tuple.input)));
Assert.Equal(tuple.expectedOutput, UIInvoiceController.PosDataParser.ParsePosData(string.IsNullOrEmpty(tuple.input) ? null : JToken.Parse(tuple.input)));
});
}
[Fact]
@ -2238,7 +2246,7 @@ bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku
Data.InvoiceData data = new Data.InvoiceData();
obj = data;
data.Blob2 = v.Input.ToString();
data.Migrate();
data.TryMigrate();
var actual = JObject.Parse(data.Blob2);
AssertSameJson(v.Expected, actual);
if (!v.SkipRountripTest)
@ -2258,7 +2266,7 @@ bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku
//data.
obj = data;
data.Blob2 = v.Input.ToString();
data.Migrate();
data.TryMigrate();
var actual = JObject.Parse(data.Blob2);
AssertSameJson(v.Expected, actual);
if (!v.SkipRountripTest)
@ -2373,7 +2381,7 @@ bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku
});
var data = new Data.InvoiceData();
data.Blob2 = o.ToString();
data.Migrate();
data.TryMigrate();
var migrated = JObject.Parse(data.Blob2);
return migrated["prompts"]["BTC-CHAIN"]["details"]["accountDerivation"].Value<string>();
})

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Net.Http;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
@ -13,9 +14,14 @@ using BTCPayServer.Events;
using BTCPayServer.Lightning;
using BTCPayServer.Models.InvoicingModels;
using BTCPayServer.NTag424;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.PayoutProcessors;
using BTCPayServer.Plugins.PointOfSale.Controllers;
using BTCPayServer.Plugins.PointOfSale.Models;
using BTCPayServer.Services;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Notifications;
using BTCPayServer.Services.Notifications.Blobs;
using BTCPayServer.Services.Stores;
@ -30,6 +36,7 @@ using Xunit;
using Xunit.Abstractions;
using Xunit.Sdk;
using CreateApplicationUserRequest = BTCPayServer.Client.Models.CreateApplicationUserRequest;
using PosViewType = BTCPayServer.Plugins.PointOfSale.PosViewType;
namespace BTCPayServer.Tests
{
@ -90,6 +97,9 @@ namespace BTCPayServer.Tests
Assert.NotNull(e.APIError.Message);
GreenfieldPermissionAPIError permissionError = Assert.IsType<GreenfieldPermissionAPIError>(e.APIError);
Assert.Equal(Policies.CanModifyStoreSettings, permissionError.MissingPermission);
var client = await user.CreateClient(Policies.CanViewStoreSettings);
await AssertAPIError("unsupported-in-v2", () => client.SendHttpRequest<object>($"api/v1/stores/{user.StoreId}/payment-methods/LightningNetwork"));
}
[Fact(Timeout = TestTimeout)]
@ -361,6 +371,27 @@ namespace BTCPayServer.Tests
}
)
);
var template = @"[
{
""description"": ""Lovely, fresh and tender, Meng Ding Gan Lu ('sweet dew') is grown in the lush Meng Ding Mountains of the southwestern province of Sichuan where it has been cultivated for over a thousand years."",
""id"": ""green-tea"",
""image"": ""~/img/pos-sample/green-tea.jpg"",
""priceType"": ""Fixed"",
""price"": ""1"",
""title"": ""Green Tea"",
""disabled"": false
}
]";
await AssertValidationError(new[] { "Template" },
async () => await client.CreatePointOfSaleApp(
user.StoreId,
new PointOfSaleAppRequest
{
AppName = "good name",
Template = template.Replace(@"""id"": ""green-tea"",", "")
}
)
);
// Test creating a POS app successfully
var app = await client.CreatePointOfSaleApp(
@ -369,7 +400,8 @@ namespace BTCPayServer.Tests
{
AppName = "test app from API",
Currency = "JPY",
Title = "test app title"
Title = "test app title",
Template = template
}
);
Assert.Equal("test app from API", app.AppName);
@ -552,6 +584,27 @@ namespace BTCPayServer.Tests
}
)
);
var template = @"[
{
""description"": ""Lovely, fresh and tender, Meng Ding Gan Lu ('sweet dew') is grown in the lush Meng Ding Mountains of the southwestern province of Sichuan where it has been cultivated for over a thousand years."",
""id"": ""green-tea"",
""image"": ""~/img/pos-sample/green-tea.jpg"",
""priceType"": ""Fixed"",
""price"": ""1"",
""title"": ""Green Tea"",
""disabled"": false
}
]";
await AssertValidationError(new[] { "PerksTemplate" },
async () => await client.CreateCrowdfundApp(
user.StoreId,
new CrowdfundAppRequest
{
AppName = "good name",
PerksTemplate = template.Replace(@"""id"": ""green-tea"",", "")
}
)
);
// Test creating a crowdfund app
var app = await client.CreateCrowdfundApp(
@ -559,7 +612,8 @@ namespace BTCPayServer.Tests
new CrowdfundAppRequest
{
AppName = "test app from API",
Title = "test app title"
Title = "test app title",
PerksTemplate = template
}
);
Assert.Equal("test app from API", app.AppName);
@ -678,6 +732,93 @@ namespace BTCPayServer.Tests
Assert.False(apps[2].Archived);
}
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanGetAppStats()
{
using var tester = CreateServerTester();
await tester.StartAsync();
var user = tester.NewAccount();
await user.GrantAccessAsync(true);
await user.MakeAdmin();
await user.RegisterDerivationSchemeAsync("BTC");
var client = await user.CreateClient();
var item1 = new ViewPointOfSaleViewModel.Item { Id = "item1", Title = "Item 1", Price = 1, PriceType = ViewPointOfSaleViewModel.ItemPriceType.Fixed };
var item2 = new ViewPointOfSaleViewModel.Item { Id = "item2", Title = "Item 2", Price = 2, PriceType = ViewPointOfSaleViewModel.ItemPriceType.Fixed };
var item3 = new ViewPointOfSaleViewModel.Item { Id = "item3", Title = "Item 3", Price = 3, PriceType = ViewPointOfSaleViewModel.ItemPriceType.Fixed };
var posItems = AppService.SerializeTemplate([item1, item2, item3]);
var posApp = await client.CreatePointOfSaleApp(user.StoreId, new PointOfSaleAppRequest { AppName = "test pos", Template = posItems, });
var crowdfundApp = await client.CreateCrowdfundApp(user.StoreId, new CrowdfundAppRequest { AppName = "test crowdfund" });
// empty states
var posSales = await client.GetAppSales(posApp.Id);
Assert.NotNull(posSales);
Assert.Equal(0, posSales.SalesCount);
Assert.Equal(7, posSales.Series.Count());
var crowdfundSales = await client.GetAppSales(crowdfundApp.Id);
Assert.NotNull(crowdfundSales);
Assert.Equal(0, crowdfundSales.SalesCount);
Assert.Equal(7, crowdfundSales.Series.Count());
var posTopItems = await client.GetAppTopItems(posApp.Id);
Assert.Empty(posTopItems);
var crowdfundItems = await client.GetAppTopItems(crowdfundApp.Id);
Assert.Empty(crowdfundItems);
// with sales - fiddle invoices via the UI controller
var uiPosController = tester.PayTester.GetController<UIPointOfSaleController>();
var action = Assert.IsType<RedirectToActionResult>(uiPosController.ViewPointOfSale(posApp.Id, PosViewType.Static, 1, choiceKey: item1.Id).GetAwaiter().GetResult());
Assert.Equal(nameof(UIInvoiceController.Checkout), action.ActionName);
Assert.True(action.RouteValues!.TryGetValue("invoiceId", out var i1Id));
var cart = new JObject {
["cart"] = new JArray
{
new JObject { ["id"] = item2.Id, ["count"] = 4 },
new JObject { ["id"] = item3.Id, ["count"] = 2 }
},
["subTotal"] = 14,
["total"] = 14,
["amounts"] = new JArray()
}.ToString();
action = Assert.IsType<RedirectToActionResult>(uiPosController.ViewPointOfSale(posApp.Id, PosViewType.Cart, 7, posData: cart).GetAwaiter().GetResult());
Assert.Equal(nameof(UIInvoiceController.Checkout), action.ActionName);
Assert.True(action.RouteValues!.TryGetValue("invoiceId", out var i2Id));
await user.PayInvoice(i1Id!.ToString());
await user.PayInvoice(i2Id!.ToString());
posSales = await client.GetAppSales(posApp.Id);
Assert.Equal(7, posSales.SalesCount);
Assert.Equal(7, posSales.Series.Count());
Assert.Equal(0, posSales.Series.First().SalesCount);
Assert.Equal(7, posSales.Series.Last().SalesCount);
posTopItems = await client.GetAppTopItems(posApp.Id);
Assert.Equal(3, posTopItems.Count);
Assert.Equal(item2.Id, posTopItems[0].ItemCode);
Assert.Equal(4, posTopItems[0].SalesCount);
Assert.Equal(item3.Id, posTopItems[1].ItemCode);
Assert.Equal(2, posTopItems[1].SalesCount);
Assert.Equal(item1.Id, posTopItems[2].ItemCode);
Assert.Equal(1, posTopItems[2].SalesCount);
// with count and offset
posTopItems = await client.GetAppTopItems(posApp.Id,1, 5);
Assert.Equal(2, posTopItems.Count);
Assert.Equal(item3.Id, posTopItems[0].ItemCode);
Assert.Equal(2, posTopItems[0].SalesCount);
Assert.Equal(item1.Id, posTopItems[1].ItemCode);
Assert.Equal(1, posTopItems[1].SalesCount);
}
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanDeleteUsersViaApi()
@ -1010,7 +1151,7 @@ namespace BTCPayServer.Tests
Description = "Test description",
Amount = 12.3m,
Currency = "BTC",
PaymentMethods = new[] { "BTC" }
PayoutMethods = new[] { "BTC" }
});
void VerifyResult()
@ -1041,7 +1182,7 @@ namespace BTCPayServer.Tests
Name = "Test 2",
Amount = 12.3m,
Currency = "BTC",
PaymentMethods = new[] { "BTC" },
PayoutMethods = new[] { "BTC" },
BOLT11Expiration = TimeSpan.FromDays(31.0)
});
Assert.Equal(TimeSpan.FromDays(31.0), test2.BOLT11Expiration);
@ -1071,30 +1212,30 @@ namespace BTCPayServer.Tests
{
Destination = destination,
Amount = 1_000_000m,
PaymentMethod = "BTC",
PayoutMethodId = "BTC",
}));
await this.AssertAPIError("archived", async () => await unauthenticated.CreatePayout(pps[1].Id, new CreatePayoutRequest()
{
Destination = destination,
PaymentMethod = "BTC"
PayoutMethodId = "BTC"
}));
var payout = await unauthenticated.CreatePayout(pps[0].Id, new CreatePayoutRequest()
{
Destination = destination,
PaymentMethod = "BTC"
PayoutMethodId = "BTC"
});
payouts = await unauthenticated.GetPayouts(pps[0].Id);
var payout2 = Assert.Single(payouts);
Assert.Equal(payout.Amount, payout2.Amount);
Assert.Equal(payout.OriginalAmount, payout2.OriginalAmount);
Assert.Equal(payout.Id, payout2.Id);
Assert.Equal(destination, payout2.Destination);
Assert.Equal(PayoutState.AwaitingApproval, payout.State);
Assert.Equal("BTC-CHAIN", payout2.PaymentMethod);
Assert.Equal("BTC", payout2.CryptoCode);
Assert.Null(payout.PaymentMethodAmount);
Assert.Equal("BTC-CHAIN", payout2.PayoutMethodId);
Assert.Equal("BTC", payout2.PayoutCurrency);
Assert.Null(payout.PayoutAmount);
TestLogs.LogInformation("Can't overdraft");
@ -1103,14 +1244,14 @@ namespace BTCPayServer.Tests
{
Destination = destination2,
Amount = 0.00001m,
PaymentMethod = "BTC"
PayoutMethodId = "BTC"
}));
TestLogs.LogInformation("Can't create too low payout");
await this.AssertAPIError("amount-too-low", async () => await unauthenticated.CreatePayout(pps[0].Id, new CreatePayoutRequest()
{
Destination = destination2,
PaymentMethod = "BTC"
PayoutMethodId = "BTC"
}));
TestLogs.LogInformation("Can archive payout");
@ -1126,7 +1267,7 @@ namespace BTCPayServer.Tests
payout = await unauthenticated.CreatePayout(pps[0].Id, new CreatePayoutRequest()
{
Destination = destination,
PaymentMethod = "BTC"
PayoutMethodId = "BTC"
});
var start = RoundSeconds(DateTimeOffset.Now + TimeSpan.FromDays(7.0));
@ -1136,7 +1277,7 @@ namespace BTCPayServer.Tests
Amount = 12.3m,
StartsAt = start,
Currency = "BTC",
PaymentMethods = new[] { "BTC" }
PayoutMethods = new[] { "BTC" }
});
Assert.Equal(start, inFuture.StartsAt);
Assert.Null(inFuture.ExpiresAt);
@ -1144,7 +1285,7 @@ namespace BTCPayServer.Tests
{
Amount = 1.0m,
Destination = destination,
PaymentMethod = "BTC"
PayoutMethodId = "BTC"
}));
var expires = RoundSeconds(DateTimeOffset.Now - TimeSpan.FromDays(7.0));
@ -1154,13 +1295,13 @@ namespace BTCPayServer.Tests
Amount = 12.3m,
ExpiresAt = expires,
Currency = "BTC",
PaymentMethods = new[] { "BTC" }
PayoutMethods = new[] { "BTC" }
});
await this.AssertAPIError("expired", async () => await unauthenticated.CreatePayout(inPast.Id, new CreatePayoutRequest()
{
Amount = 1.0m,
Destination = destination,
PaymentMethod = "BTC"
PayoutMethodId = "BTC"
}));
await this.AssertValidationError(new[] { "ExpiresAt" }, async () => await client.CreatePullPayment(storeId, new Client.Models.CreatePullPaymentRequest()
@ -1178,7 +1319,7 @@ namespace BTCPayServer.Tests
Name = "Test USD",
Amount = 5000m,
Currency = "USD",
PaymentMethods = new[] { "BTC" }
PayoutMethods = new[] { "BTC" }
});
await this.AssertAPIError("lnurl-not-supported", async () => await unauthenticated.GetPullPaymentLNURL(pp.Id));
@ -1188,7 +1329,7 @@ namespace BTCPayServer.Tests
payout = await unauthenticated.CreatePayout(pp.Id, new CreatePayoutRequest()
{
Destination = destination,
PaymentMethod = "BTC"
PayoutMethodId = "BTC"
});
await this.AssertAPIError("old-revision", async () => await client.ApprovePayout(storeId, payout.Id, new ApprovePayoutRequest()
{
@ -1203,8 +1344,8 @@ namespace BTCPayServer.Tests
Revision = payout.Revision
});
Assert.Equal(PayoutState.AwaitingPayment, payout.State);
Assert.NotNull(payout.PaymentMethodAmount);
Assert.Equal(1.0m, payout.PaymentMethodAmount); // 1 BTC == 5000 USD in tests
Assert.NotNull(payout.PayoutAmount);
Assert.Equal(1.0m, payout.PayoutAmount); // 1 BTC == 5000 USD in tests
await this.AssertAPIError("invalid-state", async () => await client.ApprovePayout(storeId, payout.Id, new ApprovePayoutRequest()
{
Revision = payout.Revision
@ -1216,18 +1357,18 @@ namespace BTCPayServer.Tests
Name = "Test 2",
Amount = 12.303228134m,
Currency = "BTC",
PaymentMethods = new[] { "BTC" }
PayoutMethods = new[] { "BTC" }
});
destination = (await tester.ExplorerNode.GetNewAddressAsync()).ToString();
payout = await unauthenticated.CreatePayout(test3.Id, new CreatePayoutRequest()
{
Destination = destination,
PaymentMethod = "BTC"
PayoutMethodId = "BTC"
});
payout = await client.ApprovePayout(storeId, payout.Id, new ApprovePayoutRequest());
// The payout should round the value of the payment down to the network of the payment method
Assert.Equal(12.30322814m, payout.PaymentMethodAmount);
Assert.Equal(12.303228134m, payout.Amount);
Assert.Equal(12.30322814m, payout.PayoutAmount);
Assert.Equal(12.303228134m, payout.OriginalAmount);
await client.MarkPayoutPaid(storeId, payout.Id);
payout = (await client.GetPayouts(payout.PullPaymentId)).First(data => data.Id == payout.Id);
@ -1240,7 +1381,7 @@ namespace BTCPayServer.Tests
Name = "Test 3",
Amount = 12.303228134m,
Currency = "BTC",
PaymentMethods = new[] { "BTC", "BTC-LightningNetwork", "BTC_LightningLike" }
PayoutMethods = new[] { "BTC", "BTC-LightningNetwork", "BTC_LightningLike" }
});
var lnrURLs = await unauthenticated.GetPullPaymentLNURL(test4.Id);
Assert.IsType<string>(lnrURLs.LNURLBech32);
@ -1315,7 +1456,7 @@ namespace BTCPayServer.Tests
Name = "Test SATS",
Amount = 21000,
Currency = "SATS",
PaymentMethods = new[] { "BTC", "BTC-LightningNetwork", "BTC_LightningLike" }
PayoutMethods = new[] { "BTC", "BTC-LightningNetwork", "BTC_LightningLike" }
});
lnrURLs = await unauthenticated.GetPullPaymentLNURL(testSats.Id);
Assert.IsType<string>(lnrURLs.LNURLBech32);
@ -1333,7 +1474,7 @@ namespace BTCPayServer.Tests
Amount = 100,
Currency = "USD",
Name = "pull payment",
PaymentMethods = new[] { "BTC" },
PayoutMethods = new[] { "BTC" },
AutoApproveClaims = true
});
});
@ -1342,7 +1483,7 @@ namespace BTCPayServer.Tests
await nonApproved.CreatePayout(acc.StoreId, new CreatePayoutThroughStoreRequest()
{
Amount = 100,
PaymentMethod = "BTC",
PayoutMethodId = "BTC",
Approved = true,
Destination = new Key().GetAddress(ScriptPubKeyType.TaprootBIP86, Network.RegTest).ToString()
});
@ -1353,14 +1494,14 @@ namespace BTCPayServer.Tests
Amount = 100,
Currency = "USD",
Name = "pull payment",
PaymentMethods = new[] { "BTC" },
PayoutMethods = new[] { "BTC" },
AutoApproveClaims = true
});
await approved.CreatePayout(acc.StoreId, new CreatePayoutThroughStoreRequest()
{
Amount = 100,
PaymentMethod = "BTC",
PayoutMethodId = "BTC",
Approved = true,
Destination = new Key().GetAddress(ScriptPubKeyType.TaprootBIP86, Network.RegTest).ToString()
});
@ -1381,7 +1522,7 @@ namespace BTCPayServer.Tests
var payout = await client.CreatePayout(storeId, new CreatePayoutThroughStoreRequest()
{
Approved = false,
PaymentMethod = "BTC",
PayoutMethodId = "BTC",
Amount = 0.0001m,
Destination = address.ToString(),
@ -1408,7 +1549,7 @@ namespace BTCPayServer.Tests
payout = await client.CreatePayout(storeId, new CreatePayoutThroughStoreRequest()
{
Approved = true,
PaymentMethod = "BTC",
PayoutMethodId = "BTC",
Amount = 0.0001m,
Destination = address.ToString()
});
@ -1529,13 +1670,14 @@ namespace BTCPayServer.Tests
CssUrl = "https://example.org/style.css",
LogoUrl = "https://example.org/logo.svg",
BrandColor = "#003366",
ApplyBrandColorToBackend = true,
PaymentMethodCriteria = new List<PaymentMethodCriteriaData>
{
new()
{
Amount = 10,
Above = true,
PaymentMethod = "BTC",
PaymentMethodId = "BTC",
CurrencyCode = "USD"
}
}
@ -1544,13 +1686,14 @@ namespace BTCPayServer.Tests
Assert.Equal("https://example.org/style.css", updatedStore.CssUrl);
Assert.Equal("https://example.org/logo.svg", updatedStore.LogoUrl);
Assert.Equal("#003366", updatedStore.BrandColor);
Assert.True(updatedStore.ApplyBrandColorToBackend);
var s = (await client.GetStore(newStore.Id));
Assert.Equal("B", s.Name);
var pmc = Assert.Single(s.PaymentMethodCriteria);
//check that pmc equals the one we set
Assert.Equal(10, pmc.Amount);
Assert.True(pmc.Above);
Assert.Equal("BTC-CHAIN", pmc.PaymentMethod);
Assert.Equal("BTC-CHAIN", pmc.PaymentMethodId);
Assert.Equal("USD", pmc.CurrencyCode);
updatedStore = await client.UpdateStore(newStore.Id, new UpdateStoreRequest() { Name = "B" });
Assert.Empty(newStore.PaymentMethodCriteria);
@ -1711,7 +1854,7 @@ namespace BTCPayServer.Tests
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
var clientProfile = await user.CreateClient(Policies.CanModifyStoreWebhooks, Policies.CanCreateInvoice);
var clientProfile = await user.CreateClient(Policies.CanModifyWebhooks, Policies.CanCreateInvoice);
var hook = await clientProfile.CreateWebhook(user.StoreId, new CreateStoreWebhookRequest()
{
Url = fakeServer.ServerUri.AbsoluteUri,
@ -2138,7 +2281,7 @@ namespace BTCPayServer.Tests
{
await client.RefundInvoice(user.StoreId, "lol fake invoice id", new RefundInvoiceRequest()
{
PaymentMethod = method.PaymentMethodId,
PayoutMethodId = method.PaymentMethodId,
RefundVariant = RefundVariant.RateThen
});
});
@ -2146,7 +2289,7 @@ namespace BTCPayServer.Tests
// test validation error for when invoice is not yet in the state in which it can be refunded
var apiError = await AssertAPIError("non-refundable", () => client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest()
{
PaymentMethod = method.PaymentMethodId,
PayoutMethodId = method.PaymentMethodId,
RefundVariant = RefundVariant.RateThen
}));
Assert.Equal("Cannot refund this invoice", apiError.Message);
@ -2164,20 +2307,20 @@ namespace BTCPayServer.Tests
});
// test validation for the payment method
var validationError = await AssertValidationError(new[] { "PaymentMethod" }, async () =>
var validationError = await AssertValidationError(new[] { "PayoutMethodId" }, async () =>
{
await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest()
{
PaymentMethod = "fake payment method",
PayoutMethodId = "fake payment method",
RefundVariant = RefundVariant.RateThen
});
});
Assert.Contains("PaymentMethod: Please select one of the payment methods which were available for the original invoice", validationError.Message);
Assert.Contains("PayoutMethodId: Please select one of the payment methods which were available for the original invoice", validationError.Message);
// test RefundVariant.RateThen
var pp = await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest()
{
PaymentMethod = method.PaymentMethodId,
PayoutMethodId = method.PaymentMethodId,
RefundVariant = RefundVariant.RateThen
});
Assert.Equal("BTC", pp.Currency);
@ -2188,7 +2331,7 @@ namespace BTCPayServer.Tests
// test RefundVariant.CurrentRate
pp = await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest()
{
PaymentMethod = method.PaymentMethodId,
PayoutMethodId = method.PaymentMethodId,
RefundVariant = RefundVariant.CurrentRate
});
Assert.Equal("BTC", pp.Currency);
@ -2198,7 +2341,7 @@ namespace BTCPayServer.Tests
// test RefundVariant.Fiat
pp = await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest()
{
PaymentMethod = method.PaymentMethodId,
PayoutMethodId = method.PaymentMethodId,
RefundVariant = RefundVariant.Fiat,
Name = "my test name"
});
@ -2212,7 +2355,7 @@ namespace BTCPayServer.Tests
{
await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest()
{
PaymentMethod = method.PaymentMethodId,
PayoutMethodId = method.PaymentMethodId,
RefundVariant = RefundVariant.Custom,
});
});
@ -2221,7 +2364,7 @@ namespace BTCPayServer.Tests
pp = await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest()
{
PaymentMethod = method.PaymentMethodId,
PayoutMethodId = method.PaymentMethodId,
RefundVariant = RefundVariant.Custom,
CustomAmount = 69420,
CustomCurrency = "JPY"
@ -2233,7 +2376,7 @@ namespace BTCPayServer.Tests
// should auto-approve if currencies match
pp = await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest()
{
PaymentMethod = method.PaymentMethodId,
PayoutMethodId = method.PaymentMethodId,
RefundVariant = RefundVariant.Custom,
CustomAmount = 0.00069420m,
CustomCurrency = "BTC"
@ -2245,7 +2388,7 @@ namespace BTCPayServer.Tests
{
await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest
{
PaymentMethod = method.PaymentMethodId,
PayoutMethodId = method.PaymentMethodId,
RefundVariant = RefundVariant.RateThen,
SubtractPercentage = 101
});
@ -2255,7 +2398,7 @@ namespace BTCPayServer.Tests
// should auto-approve
pp = await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest
{
PaymentMethod = method.PaymentMethodId,
PayoutMethodId = method.PaymentMethodId,
RefundVariant = RefundVariant.RateThen,
SubtractPercentage = 6.15m
});
@ -2268,7 +2411,7 @@ namespace BTCPayServer.Tests
{
await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest
{
PaymentMethod = method.PaymentMethodId,
PayoutMethodId = method.PaymentMethodId,
RefundVariant = RefundVariant.OverpaidAmount
});
});
@ -2278,6 +2421,14 @@ namespace BTCPayServer.Tests
invoice = await client.CreateInvoice(user.StoreId, new CreateInvoiceRequest { Amount = 5000.0m, Currency = "USD" });
methods = await client.GetInvoicePaymentMethods(user.StoreId, invoice.Id);
method = methods.First();
Assert.Equal(JTokenType.Null, method.AdditionalData["accountDerivation"].Type);
Assert.NotNull(method.AdditionalData["keyPath"]);
methods = await client.GetInvoicePaymentMethods(user.StoreId, invoice.Id, includeSensitive: true);
method = methods.First();
Assert.Equal(JTokenType.String, method.AdditionalData["accountDerivation"].Type);
var clientViewOnly = await user.CreateClient(Policies.CanViewInvoices);
await AssertApiError(403, "missing-permission", () => clientViewOnly.GetInvoicePaymentMethods(user.StoreId, invoice.Id, includeSensitive: true));
await tester.WaitForEvent<NewOnChainTransactionEvent>(async () =>
{
@ -2298,7 +2449,7 @@ namespace BTCPayServer.Tests
pp = await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest
{
PaymentMethod = method.PaymentMethodId,
PayoutMethodId = method.PaymentMethodId,
RefundVariant = RefundVariant.OverpaidAmount
});
Assert.Equal("BTC", pp.Currency);
@ -2308,7 +2459,7 @@ namespace BTCPayServer.Tests
// once more with subtract percentage
pp = await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest
{
PaymentMethod = method.PaymentMethodId,
PayoutMethodId = method.PaymentMethodId,
RefundVariant = RefundVariant.OverpaidAmount,
SubtractPercentage = 21m
});
@ -2321,7 +2472,7 @@ namespace BTCPayServer.Tests
await client.MarkInvoiceStatus(user.StoreId, invoice.Id, new MarkInvoiceStatusRequest { Status = InvoiceStatus.Settled });
var refund = await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest
{
PaymentMethod = method.PaymentMethodId,
PayoutMethodId = method.PaymentMethodId,
RefundVariant = RefundVariant.CurrentRate
});
Assert.Equal(1.0m, refund.Amount);
@ -2616,6 +2767,14 @@ namespace BTCPayServer.Tests
invoiceObject = await client.GetOnChainWalletObject(user.StoreId, "BTC", new OnChainWalletObjectId("invoice", invoice.Id), false);
Assert.DoesNotContain(invoiceObject.Links.Select(l => l.Type), t => t == "address");
// Check if we can get the monitored invoice
var invoiceRepo = tester.PayTester.GetService<InvoiceRepository>();
var includeNonActivated = true;
Assert.Single(await invoiceRepo.GetMonitoredInvoices(PaymentMethodId.Parse("BTC-CHAIN"), includeNonActivated), i => i.Id == invoice.Id);
includeNonActivated = false;
Assert.DoesNotContain(await invoiceRepo.GetMonitoredInvoices(PaymentMethodId.Parse("BTC-CHAIN"), includeNonActivated), i => i.Id == invoice.Id);
Assert.DoesNotContain(await invoiceRepo.GetMonitoredInvoices(PaymentMethodId.Parse("BTC-CHAIN")), i => i.Id == invoice.Id);
//
paymentMethods = await client.GetInvoicePaymentMethods(store.Id, invoice.Id);
Assert.Single(paymentMethods);
@ -3105,6 +3264,8 @@ namespace BTCPayServer.Tests
});
Assert.Equal(firstAddress, (await viewOnlyClient.PreviewProposedStoreOnChainPaymentMethodAddresses(store.Id, "BTC", xpub)).Addresses.First().Address);
// Testing if the rewrite rule to old API path is working
await viewOnlyClient.SendHttpRequest($"api/v1/stores/{store.Id}/payment-methods/onchain/BTC/preview", new JObject() { ["config"] = xpub.ToString() }, HttpMethod.Post);
var method = await client.UpdateStorePaymentMethod(store.Id, "BTC-CHAIN", new UpdatePaymentMethodRequest() { Enabled = true, Config = JValue.CreateString(xpub.ToString())});
@ -3158,8 +3319,8 @@ namespace BTCPayServer.Tests
await client.RemoveStorePaymentMethod(store.Id, "BTC-CHAIN");
generateResponse = await client.GenerateOnChainWallet(store.Id, "BTC",
new GenerateOnChainWalletRequest() { ExistingMnemonic = allMnemonic, AccountNumber = 1 });
new GenerateOnChainWalletRequest() { ExistingMnemonic = allMnemonic, AccountNumber = 1, Label = "test" });
Assert.Equal("test", generateResponse.Config.Label);
Assert.Equal(generateResponse.Mnemonic.ToString(), allMnemonic.ToString());
Assert.Equal(new Mnemonic("all all all all all all all all all all all all").DeriveExtKey()
@ -3384,6 +3545,12 @@ namespace BTCPayServer.Tests
{
await viewOnlyClient.GetOnChainWalletReceiveAddress(walletId.StoreId, walletId.CryptoCode);
});
// Testing if the rewrite rule to old API path is working
await AssertHttpError(403, async () =>
{
await viewOnlyClient.SendHttpRequest($"api/v1/stores/{walletId.StoreId}/payment-methods/onchain/{walletId.CryptoCode}/wallet/address", null as object);
});
var address = await client.GetOnChainWalletReceiveAddress(walletId.StoreId, walletId.CryptoCode);
var address2 = await client.GetOnChainWalletReceiveAddress(walletId.StoreId, walletId.CryptoCode);
var address3 = await client.GetOnChainWalletReceiveAddress(walletId.StoreId, walletId.CryptoCode, true);
@ -4010,7 +4177,12 @@ namespace BTCPayServer.Tests
var resp = await tester.CustomerLightningD.Pay(inv.BOLT11);
Assert.Equal(PayResult.Ok, resp.Result);
var store = tester.PayTester.GetService<StoreRepository>();
Assert.True(await store.InternalNodePayoutAuthorized(admin.StoreId));
Assert.False(await store.InternalNodePayoutAuthorized("blah"));
await admin.MakeAdmin(false);
Assert.False(await store.InternalNodePayoutAuthorized(admin.StoreId));
await admin.MakeAdmin(true);
var customerInvoice = await tester.CustomerLightningD.CreateInvoice(LightMoney.FromUnit(10, LightMoneyUnit.Satoshi),
Guid.NewGuid().ToString(), TimeSpan.FromDays(40));
@ -4018,7 +4190,7 @@ namespace BTCPayServer.Tests
new CreatePayoutThroughStoreRequest()
{
Approved = true,
PaymentMethod = "BTC_LightningNetwork",
PayoutMethodId = "BTC_LightningNetwork",
Destination = customerInvoice.BOLT11
});
Assert.Equal(payout.Metadata.ToString(), new JObject().ToString()); //empty
@ -4037,7 +4209,7 @@ namespace BTCPayServer.Tests
new CreatePayoutThroughStoreRequest()
{
Approved = true,
PaymentMethod = "BTC",
PayoutMethodId = "BTC",
Destination = (await tester.ExplorerNode.GetNewAddressAsync()).ToString(),
Amount = 0.0001m,
Metadata = JObject.FromObject(new
@ -4068,10 +4240,10 @@ namespace BTCPayServer.Tests
{
Approved = true,
Amount = new Money(100, MoneyUnit.Satoshi).ToDecimal(MoneyUnit.BTC),
PaymentMethod = "BTC_LightningNetwork",
PayoutMethodId = "BTC_LightningNetwork",
Destination = customerInvoice.BOLT11
});
Assert.Equal(payout2.Amount, new Money(100, MoneyUnit.Satoshi).ToDecimal(MoneyUnit.BTC));
Assert.Equal(payout2.OriginalAmount, new Money(100, MoneyUnit.Satoshi).ToDecimal(MoneyUnit.BTC));
}
[Fact(Timeout = 60 * 2 * 1000)]
@ -4098,7 +4270,7 @@ namespace BTCPayServer.Tests
{
Amount = 0.0001m,
Approved = true,
PaymentMethod = "BTC",
PayoutMethodId = "BTC",
Destination = (await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
});
@ -4106,7 +4278,7 @@ namespace BTCPayServer.Tests
{
Amount = 0.00001m,
Approved = false,
PaymentMethod = "BTC",
PayoutMethodId = "BTC",
Destination = (await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
});
@ -4115,7 +4287,7 @@ namespace BTCPayServer.Tests
Amount = 100,
Currency = "USD",
Name = "pull payment",
PaymentMethods = new[] { "BTC" }
PayoutMethods = new[] { "BTC" }
});
var notapprovedPayoutWithPullPayment = await adminClient.CreatePayout(admin.StoreId, new CreatePayoutThroughStoreRequest()
@ -4123,7 +4295,7 @@ namespace BTCPayServer.Tests
PullPaymentId = pullPayment.Id,
Amount = 10,
Approved = false,
PaymentMethod = "BTC",
PayoutMethodId = "BTC",
Destination = (await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
});
await adminClient.ApprovePayout(admin.StoreId, notapprovedPayoutWithPullPayment.Id,
@ -4141,7 +4313,7 @@ namespace BTCPayServer.Tests
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(payouts.Where(data => data.PayoutAmount is null));
Assert.Empty(await adminClient.ShowOnChainWalletTransactions(admin.StoreId, "BTC"));
@ -4154,12 +4326,12 @@ namespace BTCPayServer.Tests
Assert.Equal(3600, Assert.Single(await adminClient.GetStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC")).IntervalSeconds.TotalSeconds);
var tpGen = Assert.Single(await adminClient.GetPayoutProcessors(admin.StoreId));
Assert.Equal("BTC-CHAIN", Assert.Single(tpGen.PaymentMethods));
Assert.Equal("BTC-CHAIN", Assert.Single(tpGen.PayoutMethods));
//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());
await adminClient.RemovePayoutProcessor(admin.StoreId, tpGen.Name, tpGen.PayoutMethods.First());
Assert.Empty(await adminClient.GetStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC"));
Assert.Empty(await adminClient.GetPayoutProcessors(admin.StoreId));
@ -4254,7 +4426,7 @@ namespace BTCPayServer.Tests
PullPaymentId = pullPayment.Id,
Amount = 0.5m,
Approved = true,
PaymentMethod = "BTC",
PayoutMethodId = "BTC",
Destination = (await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
});
TestLogs.LogInformation("Waiting before hook...");
@ -4300,7 +4472,7 @@ namespace BTCPayServer.Tests
{
Amount = 0.1m,
Approved = true,
PaymentMethod = "BTC",
PayoutMethodId = "BTC",
Destination = (await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
});
@ -4316,7 +4488,7 @@ namespace BTCPayServer.Tests
{
Amount = 0.3m,
Approved = true,
PaymentMethod = "BTC",
PayoutMethodId = "BTC",
Destination = (await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
});
@ -4333,7 +4505,7 @@ namespace BTCPayServer.Tests
{
Amount = 0.3m,
Approved = true,
PaymentMethod = "BTC",
PayoutMethodId = "BTC",
Destination = (await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
});

@ -50,10 +50,8 @@ namespace BTCPayServer.Tests
tester.Driver.FindElement(By.Name("Name")).SendKeys("English (Custom)");
tester.ClickPagePrimary();
var translations = tester.Driver.FindElement(By.Name("Translations"));
var text = translations.Text;
text = text.Replace("Password => Password", "Password => Mot de passe");
translations.Clear();
translations.SendKeys("Password => Mot de passe");
translations.SendKeys("{ \"Password\": \"Mot de passe\" }");
tester.ClickPagePrimary();
// Check English (Custom) can be selected
@ -64,7 +62,7 @@ namespace BTCPayServer.Tests
// Check if we can remove English (Custom)
tester.LogIn();
tester.GoToServer(Views.Server.ServerNavPages.Translations);
text = tester.Driver.PageSource;
var text = tester.Driver.PageSource;
Assert.Contains("Select-Cypherpunk", text);
Assert.DoesNotContain("Select-English (Custom)", text);
// Cypherpunk is loaded from file, can't edit

@ -1,3 +1,4 @@
using System;
using System.Threading.Tasks;
using BTCPayServer.Client;
using BTCPayServer.Controllers;
@ -90,6 +91,54 @@ fruit tea:
Assert.Null( parsedDefault[4].AdditionalData);
Assert.Null( parsedDefault[4].PaymentMethods);
}
[Fact]
[Trait("Fast", "Fast")]
public void CanParseAppTemplate()
{
var template = @"[
{
""description"": ""Lovely, fresh and tender, Meng Ding Gan Lu ('sweet dew') is grown in the lush Meng Ding Mountains of the southwestern province of Sichuan where it has been cultivated for over a thousand years."",
""id"": ""green-tea"",
""image"": ""~/img/pos-sample/green-tea.jpg"",
""priceType"": ""Fixed"",
""price"": ""1"",
""title"": ""Green Tea"",
""disabled"": false
},
{
""description"": ""Tian Jian Tian Jian means 'heavenly tippy tea' in Chinese, and it describes the finest grade of dark tea. Our Tian Jian dark tea is from Hunan province which is famous for making some of the best dark teas available."",
""id"": ""black-tea"",
""image"": ""~/img/pos-sample/black-tea.jpg"",
""priceType"": ""Fixed"",
""price"": ""1"",
""title"": ""Black Tea"",
""disabled"": false
}
]";
var items = AppService.Parse(template);
Assert.Equal(2, items.Length);
Assert.Equal("green-tea", items[0].Id);
Assert.Equal("black-tea", items[1].Id);
// Fails gracefully for missing ID
var missingId = template.Replace(@"""id"": ""green-tea"",", "");
items = AppService.Parse(missingId);
Assert.Single(items);
Assert.Equal("black-tea", items[0].Id);
// Throws for missing ID
Assert.Throws<ArgumentException>(() => AppService.Parse(missingId, true, true));
// Fails gracefully for duplicate IDs
var duplicateId = template.Replace(@"""id"": ""green-tea"",", @"""id"": ""black-tea"",");
items = AppService.Parse(duplicateId);
Assert.Empty(items);
// Throws for duplicate IDs
Assert.Throws<ArgumentException>(() => AppService.Parse(duplicateId, true, true));
}
[Fact(Timeout = LongRunningTestTimeout)]
[Trait("Integration", "Integration")]

@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
@ -121,13 +122,6 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("SignTransaction")).Click();
}
private static string ExtractPSBT(SeleniumTester s)
{
var pageSource = s.Driver.PageSource;
var start = pageSource.IndexOf("id=\"psbt-base64\">");
start += "id=\"psbt-base64\">".Length;
var end = pageSource.IndexOf("<", start);
return pageSource[start..end];
}
private string ExtractPSBT(SeleniumTester s) => s.Driver.FindElement(By.Id("psbt-base64")).GetAttribute("innerText");
}
}

@ -5,10 +5,8 @@ using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client.Models;
using BTCPayServer.Lightning;
using BTCPayServer.Lightning.CLightning;
using BTCPayServer.Services;
using BTCPayServer.Views.Manage;
using BTCPayServer.Views.Server;
using BTCPayServer.Views.Stores;
@ -18,7 +16,6 @@ using NBitcoin;
using NBitcoin.RPC;
using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
using OpenQA.Selenium.Support.Extensions;
using OpenQA.Selenium.Support.UI;
using Xunit;
@ -58,6 +55,7 @@ namespace BTCPayServer.Tests
options.AddArguments($"window-size={windowSize.Width}x{windowSize.Height}");
options.AddArgument("shm-size=2g");
options.AddArgument("start-maximized");
options.AddArgument("disable-search-engine-choice-screen");
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
@ -93,7 +91,15 @@ namespace BTCPayServer.Tests
{
if (amount is not null)
{
Driver.FindElement(By.Id("test-payment-amount")).Clear();
try
{
Driver.FindElement(By.Id("test-payment-amount")).Clear();
}
// Sometimes the element is not available after a window switch... retry
catch (StaleElementReferenceException)
{
Driver.FindElement(By.Id("test-payment-amount")).Clear();
}
Driver.FindElement(By.Id("test-payment-amount")).SendKeys(amount.ToString());
}
Driver.WaitUntilAvailable(By.Id("FakePayment"));
@ -541,7 +547,6 @@ retry:
{
walletId ??= WalletId;
GoToWallet(walletId, WalletsNavPages.Receive);
Driver.FindElement(By.Id("generateButton")).Click();
var addressStr = Driver.FindElement(By.Id("Address")).GetAttribute("data-text");
var address = BitcoinAddress.Create(addressStr, ((BTCPayNetwork)Server.NetworkProvider.GetNetwork(walletId.CryptoCode)).NBitcoinNetwork);
for (var i = 0; i < coins; i++)

@ -6,7 +6,6 @@ using System.Linq;
using System.Net;
using System.Net.Http;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
@ -20,6 +19,7 @@ using BTCPayServer.NTag424;
using BTCPayServer.Payments;
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Wallets;
using BTCPayServer.Views.Manage;
using BTCPayServer.Views.Server;
@ -28,7 +28,6 @@ using BTCPayServer.Views.Wallets;
using ExchangeSharp;
using LNURL;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.EntityFrameworkCore;
using NBitcoin;
using NBitcoin.DataEncoders;
@ -373,7 +372,7 @@ namespace BTCPayServer.Tests
var usr = RandomUtils.GetUInt256().ToString().Substring(64 - 20) + "@a.com";
s.Driver.FindElement(By.Id("Email")).SendKeys(usr);
s.ClickPagePrimary();
var url = s.FindAlertMessage().FindElement(By.TagName("a")).Text;
var url = s.Driver.FindElement(By.Id("InvitationUrl")).GetAttribute("data-text");
s.Logout();
s.Driver.Navigate().GoToUrl(url);
@ -404,6 +403,84 @@ namespace BTCPayServer.Tests
Assert.Contains("/login", s.Driver.Url);
}
[Fact(Timeout = TestTimeout)]
public async Task CanManageUsers()
{
using var s = CreateSeleniumTester();
await s.StartAsync();
//Create Users
s.RegisterNewUser();
var user = s.AsTestAccount();
s.Logout();
s.GoToRegister();
s.RegisterNewUser(true);
var admin = s.AsTestAccount();
s.GoToHome();
s.GoToServer(ServerNavPages.Users);
// Manage user password reset
var rows = s.Driver.FindElements(By.CssSelector("#UsersList tr.user-overview-row"));
s.Driver.FindElement(By.Id("SearchTerm")).Clear();
s.Driver.FindElement(By.Id("SearchTerm")).SendKeys(user.RegisterDetails.Email);
s.Driver.FindElement(By.Id("SearchTerm")).SendKeys(Keys.Enter);
rows = s.Driver.FindElements(By.CssSelector("#UsersList tr.user-overview-row"));
Assert.Single(rows);
Assert.Contains(user.RegisterDetails.Email, rows.First().Text);
s.Driver.FindElement(By.CssSelector("#UsersList tr.user-overview-row:first-child .reset-password")).Click();
s.Driver.WaitForElement(By.Id("Password")).SendKeys("Password@1!");
s.Driver.FindElement(By.Id("ConfirmPassword")).SendKeys("Password@1!");
s.ClickPagePrimary();
Assert.Contains("Password successfully set", s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success).Text);
// Manage user status (disable and enable)
// Disable user
s.Driver.FindElement(By.Id("SearchTerm")).Clear();
s.Driver.FindElement(By.Id("SearchTerm")).SendKeys(user.RegisterDetails.Email);
s.Driver.FindElement(By.Id("SearchTerm")).SendKeys(Keys.Enter);
rows = s.Driver.FindElements(By.CssSelector("#UsersList tr.user-overview-row"));
Assert.Single(rows);
Assert.Contains(user.RegisterDetails.Email, rows.First().Text);
s.Driver.FindElement(By.CssSelector("#UsersList tr.user-overview-row:first-child .disable-user")).Click();
s.Driver.WaitForElement(By.Id("ConfirmContinue")).Click();
Assert.Contains("User disabled", s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success).Text);
//Enable user
s.Driver.FindElement(By.Id("SearchTerm")).Clear();
s.Driver.FindElement(By.Id("SearchTerm")).SendKeys(user.RegisterDetails.Email);
s.Driver.FindElement(By.Id("SearchTerm")).SendKeys(Keys.Enter);
rows = s.Driver.FindElements(By.CssSelector("#UsersList tr.user-overview-row"));
Assert.Single(rows);
Assert.Contains(user.RegisterDetails.Email, rows.First().Text);
s.Driver.FindElement(By.CssSelector("#UsersList tr.user-overview-row:first-child .enable-user")).Click();
s.Driver.WaitForElement(By.Id("ConfirmContinue")).Click();
Assert.Contains("User enabled", s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success).Text);
// Manage user details (edit)
s.Driver.FindElement(By.Id("SearchTerm")).Clear();
s.Driver.FindElement(By.Id("SearchTerm")).SendKeys(user.RegisterDetails.Email);
s.Driver.FindElement(By.Id("SearchTerm")).SendKeys(Keys.Enter);
rows = s.Driver.FindElements(By.CssSelector("#UsersList tr.user-overview-row"));
Assert.Single(rows);
Assert.Contains(user.RegisterDetails.Email, rows.First().Text);
s.Driver.FindElement(By.CssSelector("#UsersList tr.user-overview-row:first-child .user-edit")).Click();
s.Driver.WaitForElement(By.Id("Name")).SendKeys("Test User");
s.ClickPagePrimary();
Assert.Contains("User successfully updated", s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success).Text);
// Manage user deletion
s.GoToServer(ServerNavPages.Users);
s.Driver.FindElement(By.Id("SearchTerm")).Clear();
s.Driver.FindElement(By.Id("SearchTerm")).SendKeys(user.RegisterDetails.Email);
s.Driver.FindElement(By.Id("SearchTerm")).SendKeys(Keys.Enter);
rows = s.Driver.FindElements(By.CssSelector("#UsersList tr.user-overview-row"));
Assert.Single(rows);
Assert.Contains(user.RegisterDetails.Email, rows.First().Text);
s.Driver.FindElement(By.CssSelector("#UsersList tr.user-overview-row:first-child .delete-user")).Click();
s.Driver.WaitForElement(By.Id("ConfirmContinue")).Click();
Assert.Contains("User deleted", s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success).Text);
s.Driver.AssertNoError();
}
[Fact(Timeout = TestTimeout)]
public async Task CanRequireApprovalForNewAccounts()
{
@ -747,6 +824,14 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.CssSelector("[data-invoice-state-badge] .dropdown-toggle")).Click();
s.Driver.FindElements(By.CssSelector("[data-invoice-state-badge] .dropdown-menu button"))[0].Click();
TestUtils.Eventually(() => Assert.Contains("Settled (marked)", s.Driver.PageSource));
// zero amount invoice should redirect to receipt
var zeroAmountId = s.CreateInvoice(0);
s.GoToUrl($"/i/{zeroAmountId}");
Assert.EndsWith("/receipt", s.Driver.Url);
Assert.Contains("$0.00", s.Driver.PageSource);
s.GoToInvoice(zeroAmountId);
Assert.Equal("Settled", s.Driver.FindElement(By.CssSelector("[data-invoice-state-badge]")).Text);
}
[Fact(Timeout = TestTimeout)]
@ -1153,6 +1238,7 @@ namespace BTCPayServer.Tests
await s.StartAsync();
var userId = s.RegisterNewUser(true);
s.CreateNewStore();
s.GenerateWallet();
(_, string appId) = s.CreateApp("PointOfSale");
s.Driver.FindElement(By.Id("Title")).Clear();
s.Driver.FindElement(By.Id("Title")).SendKeys("Tea shop");
@ -1164,10 +1250,20 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("CodeTabButton")).Click();
var template = s.Driver.FindElement(By.Id("TemplateConfig")).GetAttribute("value");
Assert.Contains("\"buyButtonText\": \"Take my money\"", template);
Assert.Matches("\"categories\": \\[\n\\s+\"Drinks\"\n\\s+\\]", template);
Assert.Matches("\"categories\": \\[\r?\n\\s*\"Drinks\"\\s*\\]", template);
s.ClickPagePrimary();
Assert.Contains("App updated", s.FindAlertMessage().Text);
s.Driver.ScrollTo(By.Id("CodeTabButton"));
s.Driver.FindElement(By.Id("CodeTabButton")).Click();
template = s.Driver.FindElement(By.Id("TemplateConfig")).GetAttribute("value");
s.Driver.FindElement(By.Id("TemplateConfig")).Clear();
s.Driver.FindElement(By.Id("TemplateConfig")).SendKeys(template.Replace(@"""id"": ""green-tea"",", ""));
s.ClickPagePrimary();
Assert.Contains("Invalid template: Missing ID for item \"Green Tea\".", s.Driver.FindElement(By.CssSelector(".validation-summary-errors")).Text);
s.Driver.FindElement(By.Id("ViewApp")).Click();
var windows = s.Driver.WindowHandles;
@ -1546,7 +1642,6 @@ namespace BTCPayServer.Tests
s.GenerateWallet("BTC", "", false, true);
var walletId = new WalletId(storeId, "BTC");
s.GoToWallet(walletId, WalletsNavPages.Receive);
s.Driver.FindElement(By.Id("generateButton")).Click();
var addressStr = s.Driver.FindElement(By.Id("Address")).GetAttribute("data-text");
var address = BitcoinAddress.Create(addressStr,
((BTCPayNetwork)s.Server.NetworkProvider.GetNetwork("BTC")).NBitcoinNetwork);
@ -1751,7 +1846,6 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("WalletNav-Receive")).Click();
//generate a receiving address
s.Driver.FindElement(By.CssSelector("button[value=generate-new-address]")).Click();
Assert.True(s.Driver.FindElement(By.CssSelector("#address-tab .qr-container")).Displayed);
// no previous page in the wizard, hence no back button
Assert.True(s.Driver.ElementDoesNotExist(By.Id("GoBack")));
@ -1773,10 +1867,6 @@ namespace BTCPayServer.Tests
Assert.NotNull(s.Driver.FindElement(By.CssSelector("[data-value='test-label']")));
});
//unreserve
s.Driver.FindElement(By.CssSelector("button[value=unreserve-current-address]")).Click();
//generate it again, should be the same one as before as nothing got used in the meantime
s.Driver.FindElement(By.CssSelector("button[value=generate-new-address]")).Click();
Assert.True(s.Driver.FindElement(By.CssSelector("#address-tab .qr-container")).Displayed);
Assert.Equal(receiveAddr, s.Driver.FindElement(By.Id("Address")).GetAttribute("data-text"));
TestUtils.Eventually(() =>
@ -1866,7 +1956,7 @@ namespace BTCPayServer.Tests
// Check the tx sent earlier arrived
s.Driver.FindElement(By.Id($"StoreNav-Wallet{cryptoCode}")).Click();
s.Driver.WaitWalletTransactionsLoaded();
s.Driver.FindElement(By.PartialLinkText(tx.ToString()));
s.Driver.FindElement(By.CssSelector($"[data-text='{tx}']"));
var walletTransactionUri = new Uri(s.Driver.Url);
@ -1894,13 +1984,13 @@ namespace BTCPayServer.Tests
var jack = new Key().PubKey.Hash.GetAddress(Network.RegTest);
SetTransactionOutput(s, 0, jack, 0.01m);
s.Driver.FindElement(By.Id("SignTransaction")).Click();
s.Driver.WaitForElement(By.CssSelector("button[value=broadcast]"));
Assert.Contains(jack.ToString(), s.Driver.PageSource);
Assert.Contains("0.01000000", s.Driver.PageSource);
Assert.EndsWith("psbt/ready", s.Driver.Url);
s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).Click();
Assert.Equal(walletTransactionUri.ToString(), s.Driver.Url);
var bip21 = invoice.EntityToDTO(s.Server.PayTester.GetService<Dictionary<PaymentMethodId, IPaymentMethodBitpayAPIExtension>>()).CryptoInfo.First().PaymentUrls.BIP21;
var bip21 = invoice.EntityToDTO(s.Server.PayTester.GetService<Dictionary<PaymentMethodId, IPaymentMethodBitpayAPIExtension>>(), s.Server.PayTester.GetService<CurrencyNameTable>()).CryptoInfo.First().PaymentUrls.BIP21;
//let's make bip21 more interesting
bip21 += "&label=Solid Snake&message=Snake? Snake? SNAAAAKE!";
var parsedBip21 = new BitcoinUrlBuilder(bip21, Network.RegTest);
@ -2684,9 +2774,9 @@ namespace BTCPayServer.Tests
var sums = cartData.FindElements(By.CssSelector("tfoot tr"));
Assert.Equal(2, items.Count);
Assert.Equal(4, sums.Count);
Assert.Contains("Manual entry 1", items[0].FindElement(By.CssSelector("th")).Text);
Assert.Contains("Custom Amount 1", items[0].FindElement(By.CssSelector("th")).Text);
Assert.Contains("1 234,00 €", items[0].FindElement(By.CssSelector("td")).Text);
Assert.Contains("Manual entry 2", items[1].FindElement(By.CssSelector("th")).Text);
Assert.Contains("Custom Amount 2", items[1].FindElement(By.CssSelector("th")).Text);
Assert.Contains("0,56 €", items[1].FindElement(By.CssSelector("td")).Text);
Assert.Contains("Subtotal", sums[0].FindElement(By.CssSelector("th")).Text);
Assert.Contains("1 234,56 €", sums[0].FindElement(By.CssSelector("td")).Text);
@ -2707,9 +2797,9 @@ namespace BTCPayServer.Tests
sums = paymentDetails.FindElements(By.CssSelector("tr.sums-data"));
Assert.Equal(2, items.Count);
Assert.Equal(4, sums.Count);
Assert.Contains("Manual entry 1", items[0].FindElement(By.CssSelector(".key")).Text);
Assert.Contains("Custom Amount 1", items[0].FindElement(By.CssSelector(".key")).Text);
Assert.Contains("1 234,00 €", items[0].FindElement(By.CssSelector(".val")).Text);
Assert.Contains("Manual entry 2", items[1].FindElement(By.CssSelector(".key")).Text);
Assert.Contains("Custom Amount 2", items[1].FindElement(By.CssSelector(".key")).Text);
Assert.Contains("0,56 €", items[1].FindElement(By.CssSelector(".val")).Text);
Assert.Contains("Subtotal", sums[0].FindElement(By.CssSelector(".key")).Text);
Assert.Contains("1 234,56 €", sums[0].FindElement(By.CssSelector(".val")).Text);
@ -2770,7 +2860,7 @@ namespace BTCPayServer.Tests
Assert.Contains("1 x 1,00 € = 1,00 €", items[0].FindElement(By.CssSelector("td")).Text);
Assert.Contains("Green Tea", items[1].FindElement(By.CssSelector("th")).Text);
Assert.Contains("2 x 1,00 € = 2,00 €", items[1].FindElement(By.CssSelector("td")).Text);
Assert.Contains("Manual entry 1", items[2].FindElement(By.CssSelector("th")).Text);
Assert.Contains("Custom Amount 1", items[2].FindElement(By.CssSelector("th")).Text);
Assert.Contains("1,23 €", items[2].FindElement(By.CssSelector("td")).Text);
Assert.Contains("Total", sums[0].FindElement(By.CssSelector("th")).Text);
Assert.Contains("4,23 €", sums[0].FindElement(By.CssSelector("td")).Text);
@ -2789,7 +2879,7 @@ namespace BTCPayServer.Tests
Assert.Contains("1 x 1,00 € = 1,00 €", items[0].FindElement(By.CssSelector(".val")).Text);
Assert.Contains("Green Tea", items[1].FindElement(By.CssSelector(".key")).Text);
Assert.Contains("2 x 1,00 € = 2,00 €", items[1].FindElement(By.CssSelector(".val")).Text);
Assert.Contains("Manual entry 1", items[2].FindElement(By.CssSelector(".key")).Text);
Assert.Contains("Custom Amount 1", items[2].FindElement(By.CssSelector(".key")).Text);
Assert.Contains("1,23 €", items[2].FindElement(By.CssSelector(".val")).Text);
Assert.Contains("Total", sums[0].FindElement(By.CssSelector(".key")).Text);
Assert.Contains("4,23 €", sums[0].FindElement(By.CssSelector(".val")).Text);
@ -3014,7 +3104,7 @@ namespace BTCPayServer.Tests
// Topup Invoice test
var i = s.CreateInvoice(storeId, null, cryptoCode);
s.GoToInvoiceCheckout(i);
var lnurl = s.Driver.FindElement(By.CssSelector("#Lightning_BTC-LNURL .truncate-center-start")).Text;
var lnurl = s.Driver.FindElement(By.CssSelector("#Lightning_BTC-LNURL .truncate-center")).GetAttribute("data-text");
var parsed = LNURL.LNURL.Parse(lnurl, out var tag);
var fetchedReuqest =
Assert.IsType<LNURL.LNURLPayRequest>(await LNURL.LNURL.FetchInformation(parsed, new HttpClient()));
@ -3051,7 +3141,7 @@ namespace BTCPayServer.Tests
i = s.CreateInvoice(storeId, 0.0000001m, cryptoCode);
s.GoToInvoiceCheckout(i);
// BOLT11 is also displayed for standard invoice (not LNURL, even if it is available)
var bolt11 = s.Driver.FindElement(By.CssSelector("#Lightning_BTC-LN .truncate-center-start")).Text;
var bolt11 = s.Driver.FindElement(By.CssSelector("#Lightning_BTC-LN .truncate-center")).GetAttribute("data-text");
BOLT11PaymentRequest.Parse(bolt11, s.Server.ExplorerNode.Network);
var invoiceId = s.Driver.Url.Split('/').Last();
using (var resp = await s.Server.PayTester.HttpClient.GetAsync("BTC/lnurl/pay/i/" + invoiceId))
@ -3104,7 +3194,7 @@ namespace BTCPayServer.Tests
i = s.CreateInvoice(storeId, null, cryptoCode);
s.GoToInvoiceCheckout(i);
lnurl = s.Driver.FindElement(By.CssSelector("#Lightning_BTC-LNURL .truncate-center-start")).Text;
lnurl = s.Driver.FindElement(By.CssSelector("#Lightning_BTC-LNURL .truncate-center")).GetAttribute("data-text");
Assert.StartsWith("lnurlp", lnurl);
LNURL.LNURL.Parse(lnurl, out tag);
@ -3117,7 +3207,7 @@ namespace BTCPayServer.Tests
Assert.Contains($"{cryptoCode} Lightning settings successfully updated", s.FindAlertMessage().Text);
var invForPP = s.CreateInvoice(null, cryptoCode);
s.GoToInvoiceCheckout(invForPP);
lnurl = s.Driver.FindElement(By.CssSelector("#Lightning_BTC-LNURL .truncate-center-start")).Text;
lnurl = s.Driver.FindElement(By.CssSelector("#Lightning_BTC-LNURL .truncate-center")).GetAttribute("data-text");
LNURL.LNURL.Parse(lnurl, out tag);
// Check that pull payment has lightning option
@ -3338,11 +3428,14 @@ namespace BTCPayServer.Tests
var user = s.RegisterNewUser();
s.GoToHome();
s.GoToProfile(ManageNavPages.LoginCodes);
var code = s.Driver.FindElement(By.Id("logincode")).GetAttribute("value");
s.ClickPagePrimary();
Assert.NotEqual(code, s.Driver.FindElement(By.Id("logincode")).GetAttribute("value"));
code = s.Driver.FindElement(By.Id("logincode")).GetAttribute("value");
string code = null;
TestUtils.Eventually(() => { code = s.Driver.FindElement(By.CssSelector("#LoginCode .qr-code")).GetAttribute("alt"); });
string prevCode = code;
await s.Driver.Navigate().RefreshAsync();
TestUtils.Eventually(() => { code = s.Driver.FindElement(By.CssSelector("#LoginCode .qr-code")).GetAttribute("alt"); });
Assert.NotEqual(prevCode, code);
TestUtils.Eventually(() => { code = s.Driver.FindElement(By.CssSelector("#LoginCode .qr-code")).GetAttribute("alt"); });
s.Logout();
s.GoToLogin();
s.Driver.SetAttribute("LoginCode", "value", "bad code");

@ -25,6 +25,7 @@ namespace BTCPayServer.Tests
{
public class ServerTester : IDisposable
{
public const string DefaultConnectionString = "User ID=postgres;Include Error Detail=true;Host=127.0.0.1;Port=39372;Database=btcpayserver";
public List<IDisposable> Resources = new List<IDisposable>();
readonly string _Directory;
@ -50,19 +51,16 @@ namespace BTCPayServer.Tests
PayTester = new BTCPayServerTester(TestLogs, LoggerProvider, Path.Combine(_Directory, "pay"))
{
NBXplorerUri = ExplorerClient.Address,
TestDatabase = Enum.Parse<TestDatabases>(GetEnvironment("TESTS_DB", TestDatabases.Postgres.ToString()), true),
// TODO: The fact that we use same conn string as development database can cause huge problems with tests
// since in dev we already can have some users / stores registered, while on CI database is being initalized
// for the first time and first registered user gets admin status by default
Postgres = GetEnvironment("TESTS_POSTGRES", "User ID=postgres;Include Error Detail=true;Host=127.0.0.1;Port=39372;Database=btcpayserver"),
Postgres = GetEnvironment("TESTS_POSTGRES", DefaultConnectionString),
ExplorerPostgres = GetEnvironment("TESTS_EXPLORER_POSTGRES", "User ID=postgres;Include Error Detail=true;Host=127.0.0.1;Port=39372;Database=nbxplorer"),
MySQL = GetEnvironment("TESTS_MYSQL", "User ID=root;Host=127.0.0.1;Port=33036;Database=btcpayserver")
};
if (newDb)
{
var r = RandomUtils.GetUInt32();
PayTester.Postgres = PayTester.Postgres.Replace("btcpayserver", $"btcpayserver{r}");
PayTester.MySQL = PayTester.MySQL.Replace("btcpayserver", $"btcpayserver{r}");
TestLogs.LogInformation($"Database used: btcpayserver{r}");
}
PayTester.Port = int.Parse(GetEnvironment("TESTS_PORT", Utils.FreeTcpPort().ToString(CultureInfo.InvariantCulture)), CultureInfo.InvariantCulture);
@ -85,7 +83,7 @@ namespace BTCPayServer.Tests
File.Copy(file, Path.Combine(langdir, Path.GetFileName(file)));
}
#if ALTCOINS
public void ActivateLTC()
{
LTCExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_LTCRPCCONNECTION", "server=http://127.0.0.1:43783;ceiwHEbqWI83:DwubwWsoo3")), NetworkProvider.GetNetwork<BTCPayNetwork>("LTC").NBitcoinNetwork);
@ -100,12 +98,7 @@ namespace BTCPayServer.Tests
PayTester.Chains.Add("LBTC");
PayTester.LBTCNBXplorerUri = LBTCExplorerClient.Address;
}
public void ActivateETH()
{
PayTester.Chains.Add("ETH");
}
#endif
public void ActivateLightning()
{
ActivateLightning(LightningConnectionType.CLightning);
@ -239,7 +232,7 @@ namespace BTCPayServer.Tests
{
get; set;
}
#if ALTCOINS
public RPCClient LTCExplorerNode
{
get; set;
@ -248,15 +241,12 @@ namespace BTCPayServer.Tests
public RPCClient LBTCExplorerNode { get; set; }
public ExplorerClient LTCExplorerClient { get; set; }
public ExplorerClient LBTCExplorerClient { get; set; }
#endif
public ExplorerClient ExplorerClient
{
get; set;
}
readonly HttpClient _Http = new HttpClient();
public BTCPayServerTester PayTester
{
get; set;

@ -146,19 +146,27 @@ namespace BTCPayServer.Tests
public async Task ModifyPayment(Action<GeneralSettingsViewModel> modify)
{
var storeController = GetController<UIStoresController>();
var response = await storeController.GeneralSettings();
GeneralSettingsViewModel settings = (GeneralSettingsViewModel)((ViewResult)response).Model;
var response = await storeController.GeneralSettings(StoreId);
GeneralSettingsViewModel settings = (GeneralSettingsViewModel)((ViewResult)response).Model!;
modify(settings);
await storeController.GeneralSettings(settings);
}
public async Task ModifyGeneralSettings(Action<GeneralSettingsViewModel> modify)
{
var storeController = GetController<UIStoresController>();
var response = await storeController.GeneralSettings(StoreId);
GeneralSettingsViewModel settings = (GeneralSettingsViewModel)((ViewResult)response).Model!;
modify(settings);
storeController.GeneralSettings(settings).GetAwaiter().GetResult();
}
public async Task ModifyOnchainPaymentSettings(Action<WalletSettingsViewModel> modify)
{
var storeController = GetController<UIStoresController>();
var response = await storeController.WalletSettings(StoreId, "BTC");
WalletSettingsViewModel walletSettings = (WalletSettingsViewModel)((ViewResult)response).Model;
modify(walletSettings);
storeController.UpdatePaymentSettings(walletSettings).GetAwaiter().GetResult();
storeController.UpdateWalletSettings(walletSettings).GetAwaiter().GetResult();
}
@ -527,7 +535,7 @@ retry:
{
var server = new FakeServer();
await server.Start();
var client = await CreateClient(Policies.CanModifyStoreWebhooks);
var client = await CreateClient(Policies.CanModifyWebhooks);
var wh = await client.CreateWebhook(StoreId, new CreateStoreWebhookRequest()
{
AutomaticRedelivery = false,
@ -669,7 +677,7 @@ retry:
var db = (NpgsqlConnection)dbContext.Database.GetDbConnection();
await db.OpenAsync();
bool isHeader = true;
using (var writer = db.BeginTextImport("COPY \"Invoices\" (\"Id\",\"Blob\",\"Created\",\"CustomerEmail\",\"ExceptionStatus\",\"ItemCode\",\"OrderId\",\"Status\",\"StoreDataId\",\"Archived\",\"Blob2\") FROM STDIN DELIMITER ',' CSV HEADER"))
using (var writer = db.BeginTextImport("COPY \"Invoices\" (\"Id\",\"Blob\",\"Created\",\"ExceptionStatus\",\"Status\",\"StoreDataId\",\"Archived\",\"Blob2\") FROM STDIN DELIMITER ',' CSV HEADER"))
{
foreach (var invoice in oldInvoices)
{
@ -698,11 +706,13 @@ retry:
await writer.FlushAsync();
}
isHeader = true;
using (var writer = db.BeginTextImport("COPY \"Payments\" (\"Id\",\"Blob\",\"InvoiceDataId\",\"Accounted\",\"Blob2\",\"Type\") FROM STDIN DELIMITER ',' CSV HEADER"))
using (var writer = db.BeginTextImport("COPY \"Payments\" (\"Id\",\"Blob\",\"InvoiceDataId\",\"Accounted\",\"Blob2\",\"PaymentMethodId\") FROM STDIN DELIMITER ',' CSV HEADER"))
{
foreach (var invoice in oldPayments)
{
var localPayment = invoice.Replace("3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd", storeId);
// Old data could have Type to null.
localPayment += "UNKNOWN";
await writer.WriteLineAsync(localPayment);
}
await writer.FlushAsync();

@ -604,7 +604,7 @@
},
"expectedProperties": {
"Created": "04/23/2019 18:27:56 +00:00",
"Type": "BTC-CHAIN",
"PaymentMethodId": "BTC-CHAIN",
"Currency": "BTC",
"Status": "Settled",
"Amount": "0.07299962",
@ -634,7 +634,7 @@
},
"expectedProperties": {
"Created": "10/01/2018 14:13:22 +00:00",
"Type": "BTC-CHAIN",
"PaymentMethodId": "BTC-CHAIN",
"Currency": "BTC",
"Status": "Settled",
"Amount": "0.00017863",
@ -666,7 +666,7 @@
"Created": "03/21/2024 07:24:35 +00:00",
"CreatedInMs": "1711005875969",
"Amount": "0.00000001",
"Type": "BTC-LNURL",
"PaymentMethodId": "BTC-LNURL",
"Currency": "BTC"
}
},
@ -697,7 +697,7 @@
"Created": "03/20/2024 22:39:08 +00:00",
"CreatedInMs": "1710974348741",
"Amount": "0.00197864",
"Type": "BTC-CHAIN",
"PaymentMethodId": "BTC-CHAIN",
"Currency": "BTC",
"Status": "Settled",
"Accounted": null

@ -1,11 +1,11 @@
Id,Blob,Created,CustomerEmail,ExceptionStatus,ItemCode,OrderId,Status,StoreDataId,Archived,Blob2
Q7RqoHLngK9svM4MgRyi9y,\x1f8b0800000000000003c454cb76a24010dde72b7258cf180d08929d0a89899ae323ea4962163c0ae808ddd8344426c72f9bc57cd2fcc2340d31ea38b398cd2cfb5657d5add7fdf9fdc7fbd9f9b9845ce9ea5c1a6b9335e90db0dfd7936ca80cfd498ef45cfa52fc4818a1702bbec9893feb76d9ace3abd3d6e06e456dd76b2fece46eb8eee4d7032f9bae5f6fd44dbfb330ddd2995017a870c6691896f16200774442e4e41cae0b8c5a0cf8436dea6a4d56344d5135596e2ac286704690030f282abe349a724bd6e5a67c298cb089117746041fd815a5b2bb109304b1b6eb524812514347ef1bb2a6cd0686663e7a8b4779dcf3bd45eb5ae9bcb181e10f50c93ca6c44d1d768b3d422391817b172d2b2831880c489c229e0d4085478577890b7be51691823c418e1572d4b3c2043e60caabe29856ab578893520a58b4459a4d0d89a35bc1c54e73dec5534c84e5de8a8e520ad88c2c149ec0bb24c58ce6272c4f283e814e59399ddfe220762a48d5ebcb3f9b1a274ca380e08f24bbbaf9ec0c8b59fbdbc3d70965a20953566c8d2fbab589535b35dab61f78e9a4bb50c76b2af7bda9f18a56caca9ca1acbdf202e7a6375ccd4d50eca775539e8fbf69fa4a514c326e0e8d7870d7efb747fd66369faba38d366b4c641dc6c8c67660240b3d35c78dbe1be85dc38efcc9d7e7f832095ea4d38c1088457b5f4a9d87ee52ba5afe277a4b69fb71c1164b05270c6f5275370ec425e7cab6eb706ce511605660cf2fe575829762d7b243d85fe10a1e1e2e19475d44c161b3c9a0c818301627571717919530a0981f0767b342d8af39242ab9b0cd3588d3ad9762e0f150f784218f1f4d41b160c2685a26c57b8632c5a7b000cd80ce68789817610cace642446a36737875e5bf1aa17e99dfa179cc48b568d55df1c9ed1e7fd7a7f296cb9e0d8105a410bbf7edcee4014c4aefc60e3baa5860ffa454c279bbbb978860c4d59a77d7dce9e24e135bf54a130354487aa14855323898bf95f18916c3aeac3d2b090e7fc0860176c13d1ed2e76a40566dd0f1563d9010a88585f0356af5b3ed2f000000ffff030035140a5d88060000,2018-10-01 11:32:12+00,,,,,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
Ka6GHBrFJPRwFRga1RD6Yz,\x1f8b0800000000000003c454cd769a4014dee7297258b74682a066a7427e24e6284a729a9fc5001798083364188826c727eba28fd457e830101353db45375dcef7cdbdf7bbbf3fbfff783d383c5470a09c1c2a3632cece87ec743c759e4f9d08a98e697c7b51be543f724e195cc86f5a1eb9a31177879131ef5d8e97cc0bc2c18d978f274fc3f5e96558ce9f1ecf8c953dbcb182da98b2009834264592d4fe3280604a13ecaf05dc9618431cc44337ba46abad6aed76db50b5635d72989414fbb0c069f545d5b59ed6eff4f4da10561916c698921d5eef367c0019cd311f0401833c973998b7a5e742ef7110a7fd65e9f62889c7b6f5726b777d3fc25c9fd5ca334683c2e71724a42c9511847555b24a1287d484dcaffc9d310072b80024cd1a724403f89073e52e5ee7d84789404394e4f00633915a558656fbb881fc823120b2388ae53a8a4037529157ac452df7e991cc154a3fc594b095229cecc147b4209cadf730b738db83ce79dda3dffc60becf4953f1e33f53ea1e6a1a53f216649bb7e8a08938fa384362a870298b30e7d5ec44b25aabacf00c73e045715838a31b63f6c4343b9c9b8f78d9595a2e2e07cb30f6cfce27cb6b0b3adeed93ae5dcf5ebafd65a763d1993e31b3cbb16d0fa6b65e5e5f1bd355d7551dad0f33ec112f36f39b7e61cd543b88fb23d34b23e7eb5d769cc70fca7e4518e4b8bdde2bc3c5e85e39b9ff4ff2ee95cddb1e235e484d049e95667b7cc86acd0db7ad7086d629105e61770ff58e4258900079097c9ce1069eec0e994003ccc0e7ae7359458c39cff293a3a314e51c1811db21d42c31895a3e4d6b2d7c750a7281dbf5e686c2d515e538145b5349ac947056d441c907a20ef17e5e8095c05c96ecc6c584006f0590d296c77d915dfdaf455954c7f7d93ae3b419b466af44e7b68fbf5fa97a99eb9a4d80c7b43a79af9b2d150238b5b5bac53e652cb17fba57d278b3dd9794122c6eb6a8aeb5bd8edbcbd8d79acb18e3eab05727a909063bfd47a5e868d5ec863d4779bcfb03561c4800c1e726bd8f0694cd047d9eaa054d8021222f9fda6a1f6c7e010000ffff0300ddecc9e08e060000,2018-10-01 11:54:10+00,,,,,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
Q3kZ3F8cUD57WUqcc8QLs2,\x1f8b0800000000000003c4544972da4014ddfb1494d6099110a377806463a662b0c085f142c3176a23758b564b86b83859163952ae90564b96812259649365bff77ffff9fdfaf1f3fda654929023dd96a4a9ba5da9774ddbd06a8da5b1b3ede6741855a42fa945c408850761a6461ba3db654667539f3787fd2db51cb7bdb4a2fe68d739dc0ddd64be7bbdafef079da5ee64ce843a4085338e7d3ffb2f047026c447f681c3b2c0a8c9803f6af546bd2c2baa2ccb7545add404877042900d8f28484d949ada545bd566a32248d887883b2382cff85a23e71d08498458db71284491a861921876af3f1ec361bc5bb4d427dadb57bcfba145f4fd66fe36795a6599879438b1cd1eb04b68202270efb465694a0c020d223bfd6f64460c28260e94e6ccdc22bc11feb95597e327c5a7ff7a8708d9a6cf51d7f423f88029af916395b29c23764c2960d12449376612478f22332b3ef09e5ecb4b306333b80829603d30917f05ef9218337ab8c2ac507805e545b26bff7711bbf649def9ca9f29e50a35f108fe0852d4cd27a999cc3cdd25be5c28114d98b3748736a25bfb30b6ea5adbda786e3ceb2eebd31d5507ee5c7b45dbea563750d2deba9e7ddf1b6d173a54add5aea62ea6df1bad6db5aa93696da485c3fe60d09e0c6ac962519fec1b8632535b304516b63c2d5ab6627daa0c1cafd5d5ac6033fbfa1c5622ef45ba9e1102b176ef6ba9f3d85d4bb7ebff94de5a3a7edcb3c9629113863729bf221bc22ce79c2b3a1c9a8700304bb1e797ec56c18db1635a3e9cae700e8fce978ca30ea2603363364c237a8c85d1edb76f417134517633659b04592e6c7f07e290e54c1a5cfed59830e4f2a349534c336134ce82e213220bf129334013a006f5cfe3228c81951d0848d96236af2eb32b139addad64d343c848be68f95df1c9158fbfab5576cb59cf46c03c924adffbb1a05c8059e6ad14d845c502fb27dd12cec7e25e028211d76ede5dbd50c942215b6aae901e4a053e55a43c189ccddf4cf844d361e76ccf8cbc730bd833c00e389743fa5c0d48f20dbadcaa47e20335b1103ea52cdf1c7f030000ffff03003e6b8efb96060000,2018-10-01 11:54:32+00,,,,,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
FSktP1Nrxu7arh7TUFAgTZ,\x1f8b0800000000000003c4554b72e33610ddcf295c5c27b23e16297a1599923c63591efd55f1781620d1143124011a0035545c3a59163952ae10008415d9a54caab2c992af5f773ff48f7ffefec7cb878b0b8760e7fac2192d52396d3df0aaf4104fbce56ad4df2e1f9d9f344348c6e193a175c47615047275b37517bdfbbb948738ee6f42713779bed98feee3dde2f9dbad5b8d6f36435c3b338e81d7ce41a922e59f2d50872e00f0946524da2b46d3601c49501f5dd76b377a6dbfedf7bc96e7f9c646e88e91089624d79456b7d3ebf8dd66a76b8c501544391346dfda7d6bc7503041641f630e4298e70c969bd093de7886d63de1a2ea7b67ddc28fb76595dcfdba1db9eeb296597086cb487ea231e3b9c9a0bc75f5b42409f90044a4e34d9090c029c370b1902825746bfc2d2b50b862d132cb2c5a247b41229429344699805798ab376afdcd66a369b1a8e41ca82993335ccd1d851e8cb6b0dcab7a9e53662c0f287f97d4c0c31c119d56c5d54d01fe0b54282f3268442c774e99012ba9e4fb33311e497106550f97e73206449e0b62bbd1fe6753eb8c699a30fa9ae45809d5dd0192e884ae5acec9ce946521f55c6d4dfdaaa20cdd413fdc2671390f36eeec9977c6f162f08da457e9704576fd344ea2db8f93743d84abf0f1b9db59cf7ef3fcf4ea6ac866ddc9a0b8bf1b8ffbd37177b75ebbd3ca5bb5e61d1f6624a46132101bbf1cce5a639cf8c120ccb7f39fbf146d917c75ce2b226046f1e5c9b959064fcef5d3ff24efc939bcae3b92a5d144e1bb63372b82a2d66c6dc70a17689f03951afbf2b5de5f884b8a5198c1e9585b78f26f63a778987088e46a7eaf89899485b8bebc3ce15dca04d154ec59597bc86a04765dcc77acb43d304962b55a5ab4d6267959cba027861fa4504b9985284a85ad09f01df015cf4ef96a852805d9c090b3462823558a9ad760bc5e7c27e2fb42323b95762d559b8f1f3f3e77f531a80b3c0199307d465f0e47530c30afbd5b47ec5d310cf69f0e9f713e1c972b6794a8ff803a69c3e3993d9e58bf6b4f6c42f4cf429f349b0cde0c0bdaa9f6ebc9b0d68f48246f195049a018f0fbfefd3d47b0b3e3f67e04972c038e687d391bcd0f87bf000000ffff030075db901fe2060000,2018-10-01 11:57:15+00,customer@example.com,,,CustomOrderId,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
HuzCsv9hghew2FD6zVqUyY,\x1f8b0800000000000003c455cb76d33010ddf3153d5e439aa75377456aa794268126a913286521dbe358b52d3992ecc6edc997b1e093f80564590d694f8073d8b0f49d3b3357f3f28f6fdf1f5f1d1d1938304e8f8c8bfcc1e68515ad22b86f9f3be6c362ed969f8dd715830bcae0bda275f8cab56de19eadccf9c9f832665e100e961ebf9caccfcaf37158ccd777efcccde86c390c6a67ca0260b5b39dcb48e9470dd4a13380e08a26d82f25a3a9308604c88f9ed96f374eda56db3ae9b7fa7d4bd9302928f6e11aa715a5d5eb9c74ac5edfea28236c322c9d3125cfeca6a9ed01649463310802069cabe758e9f2be701d7ad3b3379f9069a72be406a865ad3fdf59e36ceab46a9919a341ee8bf724a42c5519a47755bd4a9280d401ee57f126880b60840670341728c664a5fc35cb96b864913c49349a4525c73e4a241aa284c313cce41b2bfdcd66a3a9313f670c882a933174678644b74a9b9797b29e879429cb0794be48aae0618a709556c6ad9a02ec2d6c509a25d0f0696aec336d9a13c1ca03316e707600950f178732da581c0aa2bbd1febda975c0741551f294645709d95d0709b447972d67b85065998b6aae56aa7e9b2cf74c67e0ada2309fd94b73ba669d513877ee70dc8d872e2e067118f9ef2e26f162085def66ddeb2ca60f7d2bee768774da9b38d9f872341a5c8d7ac562615e6dfa6e6bd6b1608a3de2450e5f5af970da1a0591653b5eba9abdf992b579f4d538ac08831ac5c75be3ecdabe354e6fff93bc5b63fbb4ee48e44a13817b436f960f59ad59db7615ce5099021115f6e56bbdbf10e624405e02fb63ade1c9dfc64ef202ccc017ee6c5c112321327e7a7cbcc73b161122312f695e7b88cd39e87551dfa1d4f6810a1ccad5aa4457da04cb6b1964cff087147229130ff931d7350156007359b2cf972b4408884600296d78c297a5a8790dcaeac5377c566682eaa9d46b29dbbcfbf8f3b9ab8f415de00988885667f471bb338500b3dabbb5c35e144361ff74f894f376b75c292558fe07e4491beeceac3eb1dd66b3ad4f6c84ab9f4575d2743278362ca890edaf26435b2f108f9e336023800410bcecdfaf3982428fdbcb11bca6093044eacbd968bedafe040000ffff030069b828dae2060000,2018-10-01 12:09:53+00,customer@example.com,,,CustomOrderId,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
YZV1PQasUKEXdSkSV69XDL,\x1f8b0800000000000003c455cb76db3610dde72b7cb86e65911429cbabca1463c792523d2c39759c05480c4584244003202bd5475fd6453e29bf50108455d9474dcfe9a64bdeb93373312f7efff3dbf3bbb3338b60ebf2ccfaed616dcfe648acc6e127bccc966b7ff06934b17e6a1842320e1f34cd159b5510c8d5d5c65f5e4c6e331ee164781f89dbe9d3d5eefd24a9974f5faffdedf8ea3ec4ad33e31878eb1c542a52f1ab01dad025009eb19cc43bc5e86a8c2309eac3f37da7e3f56cd7f55dcf751c6d23b46624863b523414db732fdc81ef38b636c2b624ca9930facadeb78d1d43c90491438c3908a19f33b85927f2b62f2807a7866c7ce1f5c49dbc76275e9a08e7a2ba6e65969ce12a961f68c278a13328efa67a8d2409c50844dcc49b2221815386e16c295146e846fb1b56a070c5a2559e1bb44c7782c42857688272012f30576f6cf477bb9daec1e28a73a0ba4c56b85a580add6b6d51b553f53ca54c5b3ea2e24d520d8705224d5a15b7690af05f608b8a32874ecc0aeb9819b08a4abe3b11e381942750f570792a6340e4a920a61bce3f9bec13a659cae84b92432554774748a223ba6a3927b52ecb523673b5d1f5db9655e48f86d1264daa4570efcf9fb83b4e96a3af24eb65e18ad4c32c49e3eb9b69b60ea1173d3c79ee7afe477f90f57a219b7bd35139b91d8f87b3b157afd7fe6cdb5fd90b77007312d1281d89fb4115ceed314e07c1282a368b9f3f978e48bf58a71511d0a3f8fc685ddd058fd6e5e3ff24efd1dabfac3b9295d644e177cb6c560c65abd9d80e152ed1ae002a1becf397767f21a92846510ec7636de0e9bf8d9de261c22196abc5a421a65296e2f2fcfc88772e534433b16355eb21b7efc1ac8bfe4e94b68f4c9244ad5623bad12679d5caa047861fa4504b994728ce84a909f01af88ae7c77cb5429482ec6028582792b12a45cbeb30de2ebe15f35d2999994ab396aacd878f1f9fbbf618b4059e824c5973469ff7075302b068bded03f6a6181afb4f874f3bef0fcb55304ad47f409db4f07066cd89ed75bb7d736253d2fc2c9a936692c1ab6141b56a7f3319c67a8344fa9a015b0914037edbbfbfe7086a336e6f47f08ee5c0116d2f67a7fb6eff17000000ffff03003a3b8e5ae2060000,2018-10-01 12:17:01+00,customer@example.com,,,CustomOrderId,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
3qtdXmycQwhWEPs91MExB2,\x1f8b0800000000000003c455cb769b4810dde72b7c5827b21002455e4502f2b0a48c1e96e538cea2a10bd106ba5177238bf8e8cb66319f34bf90a6c18aeca324e7cc6696dcba5575bb5efcfbf73f8fafcece0c828d8b33c3da487c9395e1ec215ef953d13727fe6ed8315e570c2119874f354dac97ae2b97c3b5b3783bbe4c7880a3c12a109793cdb07c3f8eb68bcdfd0767371aae7c5c3b338e81d7ce6ea122657f35401d3a07c0539692b0548cb6c63892a03e6cc7e9b4ecae69598e655b9d8eb611ba6524842b925514d3b6de5a7dc7b11d6d845d4e943361f499bd6737760c3913440e30e620847e8ef9b0bcf70a6bf6ddc3e3e48bdfe38e7f39ef4d2837931b424641af969973868b507ea211e399cea0bcabea559224641e88b08a37414202a70cc3d942a284d0b5f66f58aec2158b1669daa0795c0a12a254a1114a053cc15cbd51eb373bad76038605e740759d0c7f393714bad7e282a254053d254d5b3ea3ec45560dfb1922555e15b7ea0af077b043599e422b649971cc745941252f4fc4b825f90954bd5c9ecae812792a48d38eceaf4de609d33466f429c9a112aabd1e92e888ae7acec956976521abc15aebfaedf22270bc41b08ea362eeae9cd9865ba368e1dd93a49bf84bb21d24511c7ef83849ae7de806b71bdbba9e7deff5936ed767337be2e5e3cbd168301dd9dbeb6b67baeb2dcdb9d587190968107b62d52ffc9939c271dff5826c3d7ff335ef88f89b715a11013d8b8f77c6f0cabd332eeefe277977c6fe69df912cb4260a0f46b35a21e4b5e6c676a8708eca0ca8acb0afdfea0586a8a01805291ccf75034ffe34768a870987502ee7e38a184b998b8bf3f323deb98c114d44c98ada43eede83de97767d4122a5ed33932452bb5589aeb4495ed432e891e13729d456a6010a13d1d404f816f892a7c77cb54294826c61c8582b90a12a45cd6b315e6fbe11f23297ac99ca662d559b0f1fbfbf77f535a80b3c0119b3ea8e3eee0fa608605e7b9b07ec453134f69f2e9f76de1f962b6394a81f81ba69fee1ce3637b6db36cde6c6c6a4fa5b5437ad4906cf86056d55fbabc968ac1f91889f3360278162c02ffbf7738e60db8cdbcb11bc62297044ebd3d96abfdaff000000ffff0300fd61e9cae3060000,2018-10-01 12:24:16+00,customer@example.com,,,CustomOrderId,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
BtJGr7kt52QkZe8RdhKz42,\x1f8b0800000000000003c455c172da4810bde72b5c3a6f301208249f168462078cd780c115e21c469a161a4b9a9167465a888b2fcb219f945fc86824b3d8c566abf692a3de7b3dfda6a7bbf5e3dbf7e777676706c1c6c5993194e34bde4fa46dcd923538731c4fbe762de38f4a2124e3f051cb3a62b3f43cb91c6e7a0be77a9cf0004783fb408ca74fc3dd87eba85c3c3d5ef6b693e1bd8feb60c631f03ad82bd449d95f0d501f9d03e05b969270a7146d8d7124417dd83dc76a5996d9711dc771db3dcd115a3212c21dc92a8969779c8edb6ff72d4dc236272a9830fa9a771b1e43ce0491038c3908a1af73152d3ee55bfea9fb784978de21ebf286925537598ef12258737f52dbcc39c345283fd288f14c6750d155f52a4b12b21188b03a6f8a84044e1986b3854409a11b1ddfa83c852b152dd2b441f3782748885285462815f002737547eddf6cb79c7ebf6d9b96dd7061c139505d2ec35fce0d85eeb5c7a0d8a9ba9e72a8991b94bd49ae613f43a44a6fa4880b4168028efbe7a6025b21cb8c63a9c70a2af9eec4216b929f405505e4a9941e91a70e699ec5fa77ca3c41ddc68cbe243994423df308497424576fcf49a9ebb29055836d7401b77911f44683601347c5dcbbefcd9e7867122d468f24e926fe929483248ac3cbab69b2f2a11bac9fecce6af6b5ef26ddaecf66f674945f8f2793c1edc42e57abdeedb6bf34e71d176624a0413c12f76ee1cfcc098e5d6f14649bf9fbcfb925e22fc6694704744f3e3f18c33befc1b878f84df61e8cfdcbdc2359684f14fe369a110b21af3d37dca1c239da654065857dfe520f324405c52848e1b8bf1b78fa9f7da784987008e5727e5d2963297371717e0e5b94e52954ba7319239a881d2bea08b9fd007a70daf52a8994b91b2649a486ac725d9993bca87dd023e21729d478a6010a13d1140578097cc9d363bdf24d29c816868cb50219aa5ad4ba16e3f50a3042becb256bdab2194cf5ce878f5f2fbe7a2dd4159e828c59b5509ff7072a0298d7d1e6017b530c8dfdaf15a883f787e9ca1825ea8fa0969b7f58b8cdb2edb64dbb59b631a97e1bd5726b92c1ab6e41a57aeaaa351af60a89f8b502b6122806fcf6fdfe6924289b7e7bdb83772c058e68bd435bed77fb9f000000ffff030034f07389ec060000,2018-10-01 12:31:12+00,customer@gmail.com,,,CustomOrderId,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
VWB3riH9WuCv9NtSedaGE6,\x1f8b0800000000000003c455c1729b4810bde72b5c9cb3b21012029f222162c792bc9664a494631f0668c404984133032bc5a52fcb219f945fc83060457629deaabdec91f75e4fbfe9e96e7e7efff1f4eeec4cc3a17671a62d574383e12b7b5538a57d231610a24bd7d4de570a2e28834f4a66f0b5e738c21baecd8535b94e981f468395cfafa79be1eee3242a179baf97e6763c5cb9611d4c5908ac0e760a7952f67703d447e700e12d4d71b0938ab6c21812203f7aa6d569753aba615b9665b74dc56152521cc01dce2a89de332cc3eeebb6ae48d8e65806634a5ef056bbe143c829c76210860c3857d7e96fd6b6037d4f1457c10adb9fbd9bfb8531b059d17326cbcbd5106a9b39a36111884f24a22c5319647455bdca92806c043ca8ce9b222e80111ac2d942a00493b58a6f548ec4a58a1469daa079bce33840a944239472788699bca3f2afb75b56bfdfeee99d5ec30505634054b934d79b6b12dd2b8f7eb193753de5503137287b955cc16e8670955e4b11e31c93042cfbc3ba025b01cdb463a9430b22d8eec421f7383f81ca0a8853291d2c4e1dd23c4be7cf947e82ba8d29794e7228857ce61112e8482edf9ee152d56521aa065bab026ef3c23747037f1d47c5dc5999b30d33c6d162f41527ddc4f5703948a238b8bc9a264b17bafefda6672c67dffa76d2edba74d69b8ef2c9f5783cb81df7cae5d2bcddf63d7d6ed830c33ef1e3115fd9853bd3c7616c3b233f5bcffffa927778fca89d768441f5e4d38336bc731eb48b87ffc9de83b67f9e7b240ae589c03f5a336201e4b5e7863b543847bb0c88a8b02f8ff52043549010f9291cf777034fffb5efa430c40c02e1cd2795321622e717e7e7b045599e42a53b17312209dfd1a28e10db8fa006a75daf92489abba1024772c82ad79539c18ada073922de4821c733f55190f0a628c04a601e4b8ff5d23721205a2164b4e58b40d6a2d6b528ab578016b05d2e68d396cd60ca773e7cbcbdf8eab55057780a22a6d5427dda1fa808605e47eb07ec553114f69f56a00ade1fa62ba304cb3f825c6eee61e136cbb6dbd6cd66d9c6b8fa6d54cbad49062fba0595f2a9abd668d82bc4e3970ad80a202184afdfef772341d9f4dbeb1ebca3293044ea1dda6abfdbff020000ffff0300e1a26ae9ec060000,2018-10-01 12:33:11+00,customer@gmail.com,,,CustomOrderId,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
At4CDM5vfewV2WDHEPKRbt,\x1f8b0800000000000003c4554d73a33810bdcfaf4871de758c0db6c9696d20c9c471c61fb15d35933908688c06908824183329ffb23dec4fdabfb092205e27e59daddacb1e79efb5faa9d5ddfcf9fb1f2f1f2e2e0c1c195717c65858ae37b3ab18be6f7a5befd69f4f9781307e510a2e28838f5ad6e7bbb5eb8af56437588deeef521644f1781bf0bbd9f3a4bebe8fabd5f3b79bc17e3ad9fa51134c5904ac09764b7952fea9059aa30b80684e331cd652d1d5184302e4873d18f53b3db33f1c398edd1d3a9ac3a4a23884479c2b8969f74756d71c0d4c4dc2bec0321853f286ef0d5b3e8282722cc651c480737d9db1fde361b901320bf8c4ab83bb38c2dc22d73dd7de06f5eac6228dcd82d1a80cc547125396eb0c325a554f5912907bc04375de0c71018cd0082e5602a598ec747cab72252e55a4ccb2162d929ae31065128d51c6e11566f28ecabf351a76468ee90c7bc3960a4bc680e86a19fe7a6948f4a02d06652dcb7acea0661e50fe2eb786fd1c6195ddc810e31c931446ce6f3b0576429a1ba752979644b0facc219f717106950510e752ba589c3ba47d95de3f53e6196a9e50f29ae4580af9ca1e12e8442e9f9ee14ad76525547fed7401f745190cbc71b04be272e96e078b67d69fc62bef1b4eadd45fe36a9cc64978733b4b373e58c1e767bbbf59fc183aa965f97461cfbce2fe6e3a1dcfa776b5d90ce6fbe1da5cf61d58e0800489c7b74ee92fcc699438ae17e4bbe5af5f8a1e4fbe1ae71d61d02df9f2644c1edd27e3eae97fb2f7641c5ec71e89527b22f0dd68272c84a2f1dc72c70a17a8ce8108857df9dacc31c425895090c1697bb7f0ec5ffb4e0a23cc2014ebe5bd52264214fceaf212f6282f3250ba4b912092f29a964d84d85f839e9b6eb3496269ee810a1ccb1953ae9539c1cac60739217e92424e6716a030e56d518055c0d62c3bd54bdf8480e84490d34e2042598b46d7a1acd90046c8ea42d0b62ddbc194ef7cfcf8f9de6bb64253e1198884aa7dfa72385231c0b289368fd8bb6268ec3f6d401d7c384e574e09963f04b9dbfce3be3deeda41bfddb509567f0db5dbda64f0a65b50259f5ab546cbde229ebc55c05e0089207aff7e7f3712546dbfbdefc1479a0143a459a19dee87c35f000000ffff0300011a2ad8eb060000,2018-10-01 13:51:01+00,customer@gmail.com,,,CustomOrderId,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
Id,Blob,Created,ExceptionStatus,Status,StoreDataId,Archived,Blob2
Q7RqoHLngK9svM4MgRyi9y,\x1f8b0800000000000003c454cb76a24010dde72b7258cf180d08929d0a89899ae323ea4962163c0ae808ddd8344426c72f9bc57cd2fcc2340d31ea38b398cd2cfb5657d5add7fdf9fdc7fbd9f9b9845ce9ea5c1a6b9335e90db0dfd7936ca80cfd498ef45cfa52fc4818a1702bbec9893feb76d9ace3abd3d6e06e456dd76b2fece46eb8eee4d7032f9bae5f6fd44dbfb330ddd2995017a870c6691896f16200774442e4e41cae0b8c5a0cf8436dea6a4d56344d5135596e2ac286704690030f282abe349a724bd6e5a67c298cb089117746041fd815a5b2bb109304b1b6eb524812514347ef1bb2a6cd0686663e7a8b4779dcf3bd45eb5ae9bcb181e10f50c93ca6c44d1d768b3d422391817b172d2b2831880c489c229e0d4085478577890b7be51691823c418e1572d4b3c2043e60caabe29856ab578893520a58b4459a4d0d89a35bc1c54e73dec5534c84e5de8a8e520ad88c2c149ec0bb24c58ce6272c4f283e814e59399ddfe220762a48d5ebcb3f9b1a274ca380e08f24bbbaf9ec0c8b59fbdbc3d70965a20953566c8d2fbab589535b35dab61f78e9a4bb50c76b2af7bda9f18a56caca9ca1acbdf202e7a6375ccd4d50eca775539e8fbf69fa4a514c326e0e8d7870d7efb747fd66369faba38d366b4c641dc6c8c67660240b3d35c78dbe1be85dc38efcc9d7e7f832095ea4d38c1088457b5f4a9d87ee52ba5afe277a4b69fb71c1164b05270c6f5275370ec425e7cab6eb706ce511605660cf2fe575829762d7b243d85fe10a1e1e2e19475d44c161b3c9a0c818301627571717919530a0981f0767b342d8af39242ab9b0cd3588d3ad9762e0f150f784218f1f4d41b160c2685a26c57b8632c5a7b000cd80ce68789817610cace642446a36737875e5bf1aa17e99dfa179cc48b568d55df1c9ed1e7fd7a7f296cb9e0d8105a410bbf7edcee4014c4aefc60e3baa5860ffa454c279bbbb978860c4d59a77d7dce9e24e135bf54a130354487aa14855323898bf95f18916c3aeac3d2b090e7fc0860176c13d1ed2e76a40566dd0f1563d9010a88585f0356af5b3ed2f000000ffff030035140a5d88060000,2018-10-01 11:32:12+00,,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
Ka6GHBrFJPRwFRga1RD6Yz,\x1f8b0800000000000003c454cd769a4014dee7297258b74682a066a7427e24e6284a729a9fc5001798083364188826c727eba28fd457e830101353db45375dcef7cdbdf7bbbf3fbfff783d383c5470a09c1c2a3632cece87ec743c759e4f9d08a98e697c7b51be543f724e195cc86f5a1eb9a31177879131ef5d8e97cc0bc2c18d978f274fc3f5e96558ce9f1ecf8c953dbcb182da98b2009834264592d4fe3280604a13ecaf05dc9618431cc44337ba46abad6aed76db50b5635d72989414fbb0c069f545d5b59ed6eff4f4da10561916c698921d5eef367c0019cd311f0401833c973998b7a5e742ef7110a7fd65e9f62889c7b6f5726b777d3fc25c9fd5ca334683c2e71724a42c9511847555b24a1287d484dcaffc9d310072b80024cd1a724403f89073e52e5ee7d84789404394e4f00633915a558656fbb881fc823120b2388ae53a8a4037529157ac452df7e991cc154a3fc594b095229cecc147b4209cadf730b738db83ce79dda3dffc60becf4953f1e33f53ea1e6a1a53f216649bb7e8a08938fa384362a870298b30e7d5ec44b25aabacf00c73e045715838a31b63f6c4343b9c9b8f78d9595a2e2e07cb30f6cfce27cb6b0b3adeed93ae5dcf5ebafd65a763d1993e31b3cbb16d0fa6b65e5e5f1bd355d7551dad0f33ec112f36f39b7e61cd543b88fb23d34b23e7eb5d769cc70fca7e4518e4b8bdde2bc3c5e85e39b9ff4ff2ee95cddb1e235e484d049e95667b7cc86acd0db7ad7086d629105e61770ff58e4258900079097c9ce1069eec0e994003ccc0e7ae7359458c39cff293a3a314e51c1811db21d42c31895a3e4d6b2d7c750a7281dbf5e686c2d515e538145b5349ac947056d441c907a20ef17e5e8095c05c96ecc6c584006f0590d296c77d915dfdaf455954c7f7d93ae3b419b466af44e7b68fbf5fa97a99eb9a4d80c7b43a79af9b2d150238b5b5bac53e652cb17fba57d278b3dd9794122c6eb6a8aeb5bd8edbcbd8d79acb18e3eab05727a909063bfd47a5e868d5ec863d4779bcfb03561c4800c1e726bd8f0694cd047d9eaa054d8021222f9fda6a1f6c7e010000ffff0300ddecc9e08e060000,2018-10-01 11:54:10+00,,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
Q3kZ3F8cUD57WUqcc8QLs2,\x1f8b0800000000000003c4544972da4014ddfb1494d6099110a377806463a662b0c085f142c3176a23758b564b86b83859163952ae90564b96812259649365bff77ffff9fdfaf1f3fda654929023dd96a4a9ba5da9774ddbd06a8da5b1b3ede6741855a42fa945c408850761a6461ba3db654667539f3787fd2db51cb7bdb4a2fe68d739dc0ddd64be7bbdafef079da5ee64ce843a4085338e7d3ffb2f047026c447f681c3b2c0a8c9803f6af546bd2c2baa2ccb7545add404877042900d8f28484d949ada545bd566a32248d887883b2382cff85a23e71d08498458db71284491a861921876af3f1ec361bc5bb4d427dadb57bcfba145f4fd66fe36795a6599879438b1cd1eb04b68202270efb465694a0c020d223bfd6f64460c28260e94e6ccdc22bc11feb95597e327c5a7ff7a8708d9a6cf51d7f423f88029af916395b29c23764c2960d12449376612478f22332b3ef09e5ecb4b306333b80829603d30917f05ef9218337ab8c2ac507805e545b26bff7711bbf649def9ca9f29e50a35f108fe0852d4cd27a999cc3cdd25be5c28114d98b3748736a25bfb30b6ea5adbda786e3ceb2eebd31d5507ee5c7b45dbea563750d2deba9e7ddf1b6d173a54add5aea62ea6df1bad6db5aa93696da485c3fe60d09e0c6ac962519fec1b8632535b304516b63c2d5ab6627daa0c1cafd5d5ac6033fbfa1c5622ef45ba9e1102b176ef6ba9f3d85d4bb7ebff94de5a3a7edcb3c9629113863729bf221bc22ce79c2b3a1c9a8700304bb1e797ec56c18db1635a3e9cae700e8fce978ca30ea2603363364c237a8c85d1edb76f417134517633659b04592e6c7f07e290e54c1a5cfed59830e4f2a349534c336134ce82e213220bf129334013a006f5cfe3228c81951d0848d96236af2eb32b139addad64d343c848be68f95df1c9158fbfab5576cb59cf46c03c924adffbb1a05c8059e6ad14d845c502fb27dd12cec7e25e028211d76ede5dbd50c942215b6aae901e4a053e55a43c189ccddf4cf844d361e76ccf8cbc730bd833c00e389743fa5c0d48f20dbadcaa47e20335b1103ea52cdf1c7f030000ffff03003e6b8efb96060000,2018-10-01 11:54:32+00,,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
FSktP1Nrxu7arh7TUFAgTZ,\x1f8b0800000000000003c4554b72e33610ddcf295c5c27b23e16297a1599923c63591efd55f1781620d1143124011a0035545c3a59163952ae10008415d9a54caab2c992af5f773ff48f7ffefec7cb878b0b8760e7fac2192d52396d3df0aaf4104fbce56ad4df2e1f9d9f344348c6e193a175c47615047275b37517bdfbbb948738ee6f42713779bed98feee3dde2f9dbad5b8d6f36435c3b338e81d7ce41a922e59f2d50872e00f0946524da2b46d3601c49501f5dd76b377a6dbfedf7bc96e7f9c646e88e91089624d79456b7d3ebf8dd66a76b8c501544391346dfda7d6bc7503041641f630e4298e70c969bd093de7886d63de1a2ea7b67ddc28fb76595dcfdba1db9eeb296597086cb487ea231e3b9c9a0bc75f5b42409f90044a4e34d9090c029c370b1902825746bfc2d2b50b862d132cb2c5a247b41229429344699805798ab376afdcd66a369b1a8e41ca82993335ccd1d851e8cb6b0dcab7a9e53662c0f287f97d4c0c31c119d56c5d54d01fe0b54282f3268442c774e99012ba9e4fb33311e497106550f97e73206449e0b62bbd1fe6753eb8c699a30fa9ae45809d5dd0192e884ae5acec9ce946521f55c6d4dfdaaa20cdd413fdc2671390f36eeec9977c6f162f08da457e9704576fd344ea2db8f93743d84abf0f1b9db59cf7ef3fcf4ea6ac866ddc9a0b8bf1b8ffbd37177b75ebbd3ca5bb5e61d1f6624a46132101bbf1cce5a639cf8c120ccb7f39fbf146d917c75ce2b226046f1e5c9b959064fcef5d3ff24efc939bcae3b92a5d144e1bb63372b82a2d66c6dc70a17689f03951afbf2b5de5f884b8a5198c1e9585b78f26f63a778987088e46a7eaf89899485b8bebc3ce15dca04d154ec59597bc86a04765dcc77acb43d304962b55a5ab4d6267959cba027861fa4504b9985284a85ad09f01df015cf4ef96a852805d9c090b3462823558a9ad760bc5e7c27e2fb42323b95762d559b8f1f3f3e77f531a80b3c0199307d465f0e47530c30afbd5b47ec5d310cf69f0e9f713e1c972b6794a8ff803a69c3e3993d9e58bf6b4f6c42f4cf429f349b0cde0c0bdaa9f6ebc9b0d68f48246f195049a018f0fbfefd3d47b0b3e3f67e04972c038e687d391bcd0f87bf000000ffff030075db901fe2060000,2018-10-01 11:57:15+00,,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
HuzCsv9hghew2FD6zVqUyY,\x1f8b0800000000000003c455cb76d33010ddf3153d5e439aa75377456aa794268126a913286521dbe358b52d3992ecc6edc997b1e093f80564590d694f8073d8b0f49d3b3357f3f28f6fdf1f5f1d1d1938304e8f8c8bfcc1e68515ad22b86f9f3be6c362ed969f8dd715830bcae0bda275f8cab56de19eadccf9c9f832665e100e961ebf9caccfcaf37158ccd777efcccde86c390c6a67ca0260b5b39dcb48e9470dd4a13380e08a26d82f25a3a9308604c88f9ed96f374eda56db3ae9b7fa7d4bd9302928f6e11aa715a5d5eb9c74ac5edfea28236c322c9d3125cfeca6a9ed01649463310802069cabe758e9f2be701d7ad3b3379f9069a72be406a865ad3fdf59e36ceab46a9919a341ee8bf724a42c5519a47755bd4a9280d401ee57f126880b60840670341728c664a5fc35cb96b864913c49349a4525c73e4a241aa284c313cce41b2bfdcd66a3a9313f670c882a933174678644b74a9b9797b29e879429cb0794be48aae0618a709556c6ad9a02ec2d6c509a25d0f0696aec336d9a13c1ca03316e707600950f178732da581c0aa2bbd1febda975c0741551f294645709d95d0709b447972d67b85065998b6aae56aa7e9b2cf74c67e0ada2309fd94b73ba669d513877ee70dc8d872e2e067118f9ef2e26f162085def66ddeb2ca60f7d2bee768774da9b38d9f872341a5c8d7ac562615e6dfa6e6bd6b1608a3de2450e5f5af970da1a0591653b5eba9abdf992b579f4d538ac08831ac5c75be3ecdabe354e6fff93bc5b63fbb4ee48e44a13817b436f960f59ad59db7615ce5099021115f6e56bbdbf10e624405e02fb63ade1c9dfc64ef202ccc017ee6c5c112321327e7a7cbcc73b161122312f695e7b88cd39e87551dfa1d4f6810a1ccad5aa4457da04cb6b1964cff087147229130ff931d7350156007359b2cf972b4408884600296d78c297a5a8790dcaeac5377c566682eaa9d46b29dbbcfbf8f3b9ab8f415de00988885667f471bb338500b3dabbb5c35e144361ff74f894f376b75c292558fe07e4491beeceac3eb1dd66b3ad4f6c84ab9f4575d2743278362ca890edaf26435b2f108f9e336023800410bcecdfaf3982428fdbcb11bca6093044eacbd968bedafe040000ffff030069b828dae2060000,2018-10-01 12:09:53+00,,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
YZV1PQasUKEXdSkSV69XDL,\x1f8b0800000000000003c455cb76db3610dde72b7cb86e65911429cbabca1463c792523d2c39759c05480c4584244003202bd5475fd6453e29bf50108455d9474dcfe9a64bdeb93373312f7efff3dbf3bbb3338b60ebf2ccfaed616dcfe648acc6e127bccc966b7ff06934b17e6a1842320e1f34cd159b5510c8d5d5c65f5e4c6e331ee164781f89dbe9d3d5eefd24a9974f5faffdedf8ea3ec4ad33e31878eb1c542a52f1ab01dad025009eb19cc43bc5e86a8c2309eac3f37da7e3f56cd7f55dcf751c6d23b46624863b523414db732fdc81ef38b636c2b624ca9930facadeb78d1d43c90491438c3908a19f33b85927f2b62f2807a7866c7ce1f5c49dbc76275e9a08e7a2ba6e65969ce12a961f68c278a13328efa67a8d2409c50844dcc49b2221815386e16c295146e846fb1b56a070c5a2559e1bb44c7782c42857688272012f30576f6cf477bb9daec1e28a73a0ba4c56b85a580add6b6d51b553f53ca54c5b3ea2e24d520d8705224d5a15b7690af05f608b8a32874ecc0aeb9819b08a4abe3b11e381942750f570792a6340e4a920a61bce3f9bec13a659cae84b92432554774748a223ba6a3927b52ecb523673b5d1f5db9655e48f86d1264daa4570efcf9fb83b4e96a3af24eb65e18ad4c32c49e3eb9b69b60ea1173d3c79ee7afe477f90f57a219b7bd35139b91d8f87b3b157afd7fe6cdb5fd90b77007312d1281d89fb4115ceed314e07c1282a368b9f3f978e48bf58a71511d0a3f8fc685ddd058fd6e5e3ff24efd1dabfac3b9295d644e177cb6c560c65abd9d80e152ed1ae002a1becf397767f21a92846510ec7636de0e9bf8d9de261c22196abc5a421a65296e2f2fcfc88772e534433b16355eb21b7efc1ac8bfe4e94b68f4c9244ad5623bad12679d5caa047861fa4504b994728ce84a909f01af88ae7c77cb5429482ec6028582792b12a45cbeb30de2ebe15f35d2999994ab396aacd878f1f9fbbf618b4059e824c5973469ff7075302b068bded03f6a6181afb4f874f3bef0fcb55304ad47f409db4f07066cd89ed75bb7d736253d2fc2c9a936692c1ab6141b56a7f3319c67a8344fa9a015b0914037edbbfbfe7086a336e6f47f08ee5c0116d2f67a7fb6eff17000000ffff03003a3b8e5ae2060000,2018-10-01 12:17:01+00,,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
3qtdXmycQwhWEPs91MExB2,\x1f8b0800000000000003c455cb769b4810dde72b7c5827b21002455e4502f2b0a48c1e96e538cea2a10bd106ba5177238bf8e8cb66319f34bf90a6c18aeca324e7cc6696dcba5575bb5efcfbf73f8fafcece0c828d8b33c3da487c9395e1ec215ef953d13727fe6ed8315e570c2119874f354dac97ae2b97c3b5b3783bbe4c7880a3c12a109793cdb07c3f8eb68bcdfd0767371aae7c5c3b338e81d7ce6ea122657f35401d3a07c0539692b0548cb6c63892a03e6cc7e9b4ecae69598e655b9d8eb611ba6524842b925514d3b6de5a7dc7b11d6d845d4e943361f499bd6737760c3913440e30e620847e8ef9b0bcf70a6bf6ddc3e3e48bdfe38e7f39ef4d2837931b424641af969973868b507ea211e399cea0bcabea559224641e88b08a37414202a70cc3d942a284d0b5f66f58aec2158b1669daa0795c0a12a254a1114a053cc15cbd51eb373bad76038605e740759d0c7f393714bad7e282a254053d254d5b3ea3ec45560dfb1922555e15b7ea0af077b043599e422b649971cc745941252f4fc4b825f90954bd5c9ecae812792a48d38eceaf4de609d33466f429c9a112aabd1e92e888ae7acec956976521abc15aebfaedf22270bc41b08ea362eeae9cd9865ba368e1dd93a49bf84bb21d24511c7ef83849ae7de806b71bdbba9e7deff5936ed767337be2e5e3cbd168301dd9dbeb6b67baeb2dcdb9d587190968107b62d52ffc9939c271dff5826c3d7ff335ef88f89b715a11013d8b8f77c6f0cabd332eeefe277977c6fe69df912cb4260a0f46b35a21e4b5e6c676a8708eca0ca8acb0afdfea0586a8a01805291ccf75034ffe34768a870987502ee7e38a184b998b8bf3f323deb98c114d44c98ada43eede83de97767d4122a5ed33932452bb5589aeb4495ed432e891e13729d456a6010a13d1d404f816f892a7c77cb54294826c61c8582b90a12a45cd6b315e6fbe11f23297ac99ca662d559b0f1fbfbf77f535a80b3c0119b3ea8e3eee0fa608605e7b9b07ec453134f69f2e9f76de1f962b6394a81f81ba69fee1ce3637b6db36cde6c6c6a4fa5b5437ad4906cf86056d55fbabc968ac1f91889f3360278162c02ffbf7738e60db8cdbcb11bc62297044ebd3d96abfdaff000000ffff0300fd61e9cae3060000,2018-10-01 12:24:16+00,,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
BtJGr7kt52QkZe8RdhKz42,\x1f8b0800000000000003c455c172da4810bde72b5c3a6f301208249f168462078cd780c115e21c469a161a4b9a9167465a888b2fcb219f945fc86824b3d8c566abf692a3de7b3dfda6a7bbf5e3dbf7e777676706c1c6c5993194e34bde4fa46dcd923538731c4fbe762de38f4a2124e3f051cb3a62b3f43cb91c6e7a0be77a9cf0004783fb408ca74fc3dd87eba85c3c3d5ef6b693e1bd8feb60c631f03ad82bd449d95f0d501f9d03e05b969270a7146d8d7124417dd83dc76a5996d9711dc771db3dcd115a3212c21dc92a8969779c8edb6ff72d4dc236272a9830fa9a771b1e43ce0491038c3908a1af73152d3ee55bfea9fb784978de21ebf286925537598ef12258737f52dbcc39c345283fd288f14c6750d155f52a4b12b21188b03a6f8a84044e1986b3854409a11b1ddfa83c852b152dd2b441f3782748885285462815f002737547eddf6cb79c7ebf6d9b96dd7061c139505d2ec35fce0d85eeb5c7a0d8a9ba9e72a8991b94bd49ae613f43a44a6fa4880b4168028efbe7a6025b21cb8c63a9c70a2af9eec4216b929f405505e4a9941e91a70e699ec5fa77ca3c41ddc68cbe243994423df308497424576fcf49a9ebb29055836d7401b77911f44683601347c5dcbbefcd9e7867122d468f24e926fe929483248ac3cbab69b2f2a11bac9fecce6af6b5ef26ddaecf66f674945f8f2793c1edc42e57abdeedb6bf34e71d176624a0413c12f76ee1cfcc098e5d6f14649bf9fbcfb925e22fc6694704744f3e3f18c33befc1b878f84df61e8cfdcbdc2359684f14fe369a110b21af3d37dca1c239da654065857dfe520f324405c52848e1b8bf1b78fa9f7da784987008e5727e5d2963297371717e0e5b94e52954ba7319239a881d2bea08b9fd007a70daf52a8994b91b2649a486ac725d9993bca87dd023e21729d478a6010a13d1140578097cc9d363bdf24d29c816868cb50219aa5ad4ba16e3f50a3042becb256bdab2194cf5ce878f5f2fbe7a2dd4159e828c59b5509ff7072a0298d7d1e6017b530c8dfdaf15a883f787e9ca1825ea8fa0969b7f58b8cdb2edb64dbb59b631a97e1bd5726b92c1ab6e41a57aeaaa351af60a89f8b502b6122806fcf6fdfe6924289b7e7bdb83772c058e68bd435bed77fb9f000000ffff030034f07389ec060000,2018-10-01 12:31:12+00,,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
VWB3riH9WuCv9NtSedaGE6,\x1f8b0800000000000003c455c1729b4810bde72b5c9cb3b21012029f222162c792bc9664a494631f0668c404984133032bc5a52fcb219f945fc83060457629deaabdec91f75e4fbfe9e96e7e7efff1f4eeec4cc3a17671a62d574383e12b7b5538a57d231610a24bd7d4de570a2e28834f4a66f0b5e738c21baecd8535b94e981f468395cfafa79be1eee3242a179baf97e6763c5cb9611d4c5908ac0e760a7952f67703d447e700e12d4d71b0938ab6c21812203f7aa6d569753aba615b9665b74dc56152521cc01dce2a89de332cc3eeebb6ae48d8e65806634a5ef056bbe143c829c76210860c3857d7e96fd6b6037d4f1457c10adb9fbd9bfb8531b059d17326cbcbd5106a9b39a36111884f24a22c5319647455bdca92806c043ca8ce9b222e80111ac2d942a00493b58a6f548ec4a58a1469daa079bce33840a944239472788699bca3f2afb75b56bfdfeee99d5ec30505634054b934d79b6b12dd2b8f7eb193753de5503137287b955cc16e8670955e4b11e31c93042cfbc3ba025b01cdb463a9430b22d8eec421f7383f81ca0a8853291d2c4e1dd23c4be7cf947e82ba8d29794e7228857ce61112e8482edf9ee152d56521aa065bab026ef3c23747037f1d47c5dc5999b30d33c6d162f41527ddc4f5703948a238b8bc9a264b17bafefda6672c67dffa76d2edba74d69b8ef2c9f5783cb81df7cae5d2bcddf63d7d6ed830c33ef1e3115fd9853bd3c7616c3b233f5bcffffa927778fca89d768441f5e4d38336bc731eb48b87ffc9de83b67f9e7b240ae589c03f5a336201e4b5e7863b543847bb0c88a8b02f8ff52043549010f9291cf777034fffb5efa430c40c02e1cd2795321622e717e7e7b045599e42a53b17312209dfd1a28e10db8fa006a75daf92489abba1024772c82ad79539c18ada073922de4821c733f55190f0a628c04a601e4b8ff5d23721205a2164b4e58b40d6a2d6b528ab578016b05d2e68d396cd60ca773e7cbcbdf8eab55057780a22a6d5427dda1fa808605e47eb07ec553114f69f56a00ade1fa62ba304cb3f825c6eee61e136cbb6dbd6cd66d9c6b8fa6d54cbad49062fba0595f2a9abd668d82bc4e3970ad80a202184afdfef772341d9f4dbeb1ebca3293044ea1dda6abfdbff020000ffff0300e1a26ae9ec060000,2018-10-01 12:33:11+00,,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
At4CDM5vfewV2WDHEPKRbt,\x1f8b0800000000000003c4554d73a33810bdcfaf4871de758c0db6c9696d20c9c471c61fb15d35933908688c06908824183329ffb23dec4fdabfb092205e27e59daddacb1e79efb5faa9d5ddfcf9fb1f2f1f2e2e0c1c195717c65858ae37b3ab18be6f7a5befd69f4f9781307e510a2e28838f5ad6e7bbb5eb8af56437588deeef521644f1781bf0bbd9f3a4bebe8fabd5f3b79bc17e3ad9fa51134c5904ac09764b7952fea9059aa30b80684e331cd652d1d5184302e4873d18f53b3db33f1c398edd1d3a9ac3a4a23884479c2b8969f74756d71c0d4c4dc2bec0321853f286ef0d5b3e8282722cc651c480737d9db1fde361b901320bf8c4ab83bb38c2dc22d73dd7de06f5eac6228dcd82d1a80cc547125396eb0c325a554f5912907bc04375de0c71018cd0082e5602a598ec747cab72252e55a4ccb2162d929ae31065128d51c6e11566f28ecabf351a76468ee90c7bc3960a4bc680e86a19fe7a6948f4a02d06652dcb7acea0661e50fe2eb786fd1c6195ddc810e31c931446ce6f3b0576429a1ba752979644b0facc219f717106950510e752ba589c3ba47d95de3f53e6196a9e50f29ae4580af9ca1e12e8442e9f9ee14ad76525547fed7401f745190cbc71b04be272e96e078b67d69fc62bef1b4eadd45fe36a9cc64978733b4b373e58c1e767bbbf59fc183aa965f97461cfbce2fe6e3a1dcfa776b5d90ce6fbe1da5cf61d58e0800489c7b74ee92fcc699438ae17e4bbe5af5f8a1e4fbe1ae71d61d02df9f2644c1edd27e3eae97fb2f7641c5ec71e89527b22f0dd68272c84a2f1dc72c70a17a8ce8108857df9dacc31c425895090c1697bb7f0ec5ffb4e0a23cc2014ebe5bd52264214fceaf212f6282f3250ba4b912092f29a964d84d85f839e9b6eb3496269ee810a1ccb1953ae9539c1cac60739217e92424e6716a030e56d518055c0d62c3bd54bdf8480e84490d34e2042598b46d7a1acd90046c8ea42d0b62ddbc194ef7cfcf8f9de6bb64253e1198884aa7dfa72385231c0b289368fd8bb6268ec3f6d401d7c384e574e09963f04b9dbfce3be3deeda41bfddb509567f0db5dbda64f0a65b50259f5ab546cbde229ebc55c05e0089207aff7e7f3712546dbfbdefc1479a0143a459a19dee87c35f000000ffff0300011a2ad8eb060000,2018-10-01 13:51:01+00,,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,

1 Id Blob Created CustomerEmail ExceptionStatus Status ItemCode StoreDataId OrderId Archived Blob2
2 Q7RqoHLngK9svM4MgRyi9y \x1f8b0800000000000003c454cb76a24010dde72b7258cf180d08929d0a89899ae323ea4962163c0ae808ddd8344426c72f9bc57cd2fcc2340d31ea38b398cd2cfb5657d5add7fdf9fdc7fbd9f9b9845ce9ea5c1a6b9335e90db0dfd7936ca80cfd498ef45cfa52fc4818a1702bbec9893feb76d9ace3abd3d6e06e456dd76b2fece46eb8eee4d7032f9bae5f6fd44dbfb330ddd2995017a870c6691896f16200774442e4e41cae0b8c5a0cf8436dea6a4d56344d5135596e2ac286704690030f282abe349a724bd6e5a67c298cb089117746041fd815a5b2bb109304b1b6eb524812514347ef1bb2a6cd0686663e7a8b4779dcf3bd45eb5ae9bcb181e10f50c93ca6c44d1d768b3d422391817b172d2b2831880c489c229e0d4085478577890b7be51691823c418e1572d4b3c2043e60caabe29856ab578893520a58b4459a4d0d89a35bc1c54e73dec5534c84e5de8a8e520ad88c2c149ec0bb24c58ce6272c4f283e814e59399ddfe220762a48d5ebcb3f9b1a274ca380e08f24bbbaf9ec0c8b59fbdbc3d70965a20953566c8d2fbab589535b35dab61f78e9a4bb50c76b2af7bda9f18a56caca9ca1acbdf202e7a6375ccd4d50eca775539e8fbf69fa4a514c326e0e8d7870d7efb747fd66369faba38d366b4c641dc6c8c67660240b3d35c78dbe1be85dc38efcc9d7e7f832095ea4d38c1088457b5f4a9d87ee52ba5afe277a4b69fb71c1164b05270c6f5275370ec425e7cab6eb706ce511605660cf2fe575829762d7b243d85fe10a1e1e2e19475d44c161b3c9a0c818301627571717919530a0981f0767b342d8af39242ab9b0cd3588d3ad9762e0f150f784218f1f4d41b160c2685a26c57b8632c5a7b000cd80ce68789817610cace642446a36737875e5bf1aa17e99dfa179cc48b568d55df1c9ed1e7fd7a7f296cb9e0d8105a410bbf7edcee4014c4aefc60e3baa5860ffa454c279bbbb978860c4d59a77d7dce9e24e135bf54a130354487aa14855323898bf95f18916c3aeac3d2b090e7fc0860176c13d1ed2e76a40566dd0f1563d9010a88585f0356af5b3ed2f000000ffff030035140a5d88060000 2018-10-01 11:32:12+00 expired 3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd f
3 Ka6GHBrFJPRwFRga1RD6Yz \x1f8b0800000000000003c454cd769a4014dee7297258b74682a066a7427e24e6284a729a9fc5001798083364188826c727eba28fd457e830101353db45375dcef7cdbdf7bbbf3fbfff783d383c5470a09c1c2a3632cece87ec743c759e4f9d08a98e697c7b51be543f724e195cc86f5a1eb9a31177879131ef5d8e97cc0bc2c18d978f274fc3f5e96558ce9f1ecf8c953dbcb182da98b2009834264592d4fe3280604a13ecaf05dc9618431cc44337ba46abad6aed76db50b5635d72989414fbb0c069f545d5b59ed6eff4f4da10561916c698921d5eef367c0019cd311f0401833c973998b7a5e742ef7110a7fd65e9f62889c7b6f5726b777d3fc25c9fd5ca334683c2e71724a42c9511847555b24a1287d484dcaffc9d310072b80024cd1a724403f89073e52e5ee7d84789404394e4f00633915a558656fbb881fc823120b2388ae53a8a4037529157ac452df7e991cc154a3fc594b095229cecc147b4209cadf730b738db83ce79dda3dffc60becf4953f1e33f53ea1e6a1a53f216649bb7e8a08938fa384362a870298b30e7d5ec44b25aabacf00c73e045715838a31b63f6c4343b9c9b8f78d9595a2e2e07cb30f6cfce27cb6b0b3adeed93ae5dcf5ebafd65a763d1993e31b3cbb16d0fa6b65e5e5f1bd355d7551dad0f33ec112f36f39b7e61cd543b88fb23d34b23e7eb5d769cc70fca7e4518e4b8bdde2bc3c5e85e39b9ff4ff2ee95cddb1e235e484d049e95667b7cc86acd0db7ad7086d629105e61770ff58e4258900079097c9ce1069eec0e994003ccc0e7ae7359458c39cff293a3a314e51c1811db21d42c31895a3e4d6b2d7c750a7281dbf5e686c2d515e538145b5349ac947056d441c907a20ef17e5e8095c05c96ecc6c584006f0590d296c77d915dfdaf455954c7f7d93ae3b419b466af44e7b68fbf5fa97a99eb9a4d80c7b43a79af9b2d150238b5b5bac53e652cb17fba57d278b3dd9794122c6eb6a8aeb5bd8edbcbd8d79acb18e3eab05727a909063bfd47a5e868d5ec863d4779bcfb03561c4800c1e726bd8f0694cd047d9eaa054d8021222f9fda6a1f6c7e010000ffff0300ddecc9e08e060000 2018-10-01 11:54:10+00 expired 3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd f
4 Q3kZ3F8cUD57WUqcc8QLs2 \x1f8b0800000000000003c4544972da4014ddfb1494d6099110a377806463a662b0c085f142c3176a23758b564b86b83859163952ae90564b96812259649365bff77ffff9fdfaf1f3fda654929023dd96a4a9ba5da9774ddbd06a8da5b1b3ede6741855a42fa945c408850761a6461ba3db654667539f3787fd2db51cb7bdb4a2fe68d739dc0ddd64be7bbdafef079da5ee64ce843a4085338e7d3ffb2f047026c447f681c3b2c0a8c9803f6af546bd2c2baa2ccb7545add404877042900d8f28484d949ada545bd566a32248d887883b2382cff85a23e71d08498458db71284491a861921876af3f1ec361bc5bb4d427dadb57bcfba145f4fd66fe36795a6599879438b1cd1eb04b68202270efb465694a0c020d223bfd6f64460c28260e94e6ccdc22bc11feb95597e327c5a7ff7a8708d9a6cf51d7f423f88029af916395b29c23764c2960d12449376612478f22332b3ef09e5ecb4b306333b80829603d30917f05ef9218337ab8c2ac507805e545b26bff7711bbf649def9ca9f29e50a35f108fe0852d4cd27a999cc3cdd25be5c28114d98b3748736a25bfb30b6ea5adbda786e3ceb2eebd31d5507ee5c7b45dbea563750d2deba9e7ddf1b6d173a54add5aea62ea6df1bad6db5aa93696da485c3fe60d09e0c6ac962519fec1b8632535b304516b63c2d5ab6627daa0c1cafd5d5ac6033fbfa1c5622ef45ba9e1102b176ef6ba9f3d85d4bb7ebff94de5a3a7edcb3c9629113863729bf221bc22ce79c2b3a1c9a8700304bb1e797ec56c18db1635a3e9cae700e8fce978ca30ea2603363364c237a8c85d1edb76f417134517633659b04592e6c7f07e290e54c1a5cfed59830e4f2a349534c336134ce82e213220bf129334013a006f5cfe3228c81951d0848d96236af2eb32b139addad64d343c848be68f95df1c9158fbfab5576cb59cf46c03c924adffbb1a05c8059e6ad14d845c502fb27dd12cec7e25e028211d76ede5dbd50c942215b6aae901e4a053e55a43c189ccddf4cf844d361e76ccf8cbc730bd833c00e389743fa5c0d48f20dbadcaa47e20335b1103ea52cdf1c7f030000ffff03003e6b8efb96060000 2018-10-01 11:54:32+00 expired 3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd f
5 FSktP1Nrxu7arh7TUFAgTZ \x1f8b0800000000000003c4554b72e33610ddcf295c5c27b23e16297a1599923c63591efd55f1781620d1143124011a0035545c3a59163952ae10008415d9a54caab2c992af5f773ff48f7ffefec7cb878b0b8760e7fac2192d52396d3df0aaf4104fbce56ad4df2e1f9d9f344348c6e193a175c47615047275b37517bdfbbb948738ee6f42713779bed98feee3dde2f9dbad5b8d6f36435c3b338e81d7ce41a922e59f2d50872e00f0946524da2b46d3601c49501f5dd76b377a6dbfedf7bc96e7f9c646e88e91089624d79456b7d3ebf8dd66a76b8c501544391346dfda7d6bc7503041641f630e4298e70c969bd093de7886d63de1a2ea7b67ddc28fb76595dcfdba1db9eeb296597086cb487ea231e3b9c9a0bc75f5b42409f90044a4e34d9090c029c370b1902825746bfc2d2b50b862d132cb2c5a247b41229429344699805798ab376afdcd66a369b1a8e41ca82993335ccd1d851e8cb6b0dcab7a9e53662c0f287f97d4c0c31c119d56c5d54d01fe0b54282f3268442c774e99012ba9e4fb33311e497106550f97e73206449e0b62bbd1fe6753eb8c699a30fa9ae45809d5dd0192e884ae5acec9ce946521f55c6d4dfdaaa20cdd413fdc2671390f36eeec9977c6f162f08da457e9704576fd344ea2db8f93743d84abf0f1b9db59cf7ef3fcf4ea6ac866ddc9a0b8bf1b8ffbd37177b75ebbd3ca5bb5e61d1f6624a46132101bbf1cce5a639cf8c120ccb7f39fbf146d917c75ce2b226046f1e5c9b959064fcef5d3ff24efc939bcae3b92a5d144e1bb63372b82a2d66c6dc70a17689f03951afbf2b5de5f884b8a5198c1e9585b78f26f63a778987088e46a7eaf89899485b8bebc3ce15dca04d154ec59597bc86a04765dcc77acb43d304962b55a5ab4d6267959cba027861fa4504b9985284a85ad09f01df015cf4ef96a852805d9c090b3462823558a9ad760bc5e7c27e2fb42323b95762d559b8f1f3f3e77f531a80b3c0199307d465f0e47530c30afbd5b47ec5d310cf69f0e9f713e1c972b6794a8ff803a69c3e3993d9e58bf6b4f6c42f4cf429f349b0cde0c0bdaa9f6ebc9b0d68f48246f195049a018f0fbfefd3d47b0b3e3f67e04972c038e687d391bcd0f87bf000000ffff030075db901fe2060000 2018-10-01 11:57:15+00 customer@example.com expired 3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd CustomOrderId f
6 HuzCsv9hghew2FD6zVqUyY \x1f8b0800000000000003c455cb76d33010ddf3153d5e439aa75377456aa794268126a913286521dbe358b52d3992ecc6edc997b1e093f80564590d694f8073d8b0f49d3b3357f3f28f6fdf1f5f1d1d1938304e8f8c8bfcc1e68515ad22b86f9f3be6c362ed969f8dd715830bcae0bda275f8cab56de19eadccf9c9f832665e100e961ebf9caccfcaf37158ccd777efcccde86c390c6a67ca0260b5b39dcb48e9470dd4a13380e08a26d82f25a3a9308604c88f9ed96f374eda56db3ae9b7fa7d4bd9302928f6e11aa715a5d5eb9c74ac5edfea28236c322c9d3125cfeca6a9ed01649463310802069cabe758e9f2be701d7ad3b3379f9069a72be406a865ad3fdf59e36ceab46a9919a341ee8bf724a42c5519a47755bd4a9280d401ee57f126880b60840670341728c664a5fc35cb96b864913c49349a4525c73e4a241aa284c313cce41b2bfdcd66a3a9313f670c882a933174678644b74a9b9797b29e879429cb0794be48aae0618a709556c6ad9a02ec2d6c509a25d0f0696aec336d9a13c1ca03316e707600950f178732da581c0aa2bbd1febda975c0741551f294645709d95d0709b447972d67b85065998b6aae56aa7e9b2cf74c67e0ada2309fd94b73ba669d513877ee70dc8d872e2e067118f9ef2e26f162085def66ddeb2ca60f7d2bee768774da9b38d9f872341a5c8d7ac562615e6dfa6e6bd6b1608a3de2450e5f5af970da1a0591653b5eba9abdf992b579f4d538ac08831ac5c75be3ecdabe354e6fff93bc5b63fbb4ee48e44a13817b436f960f59ad59db7615ce5099021115f6e56bbdbf10e624405e02fb63ade1c9dfc64ef202ccc017ee6c5c112321327e7a7cbcc73b161122312f695e7b88cd39e87551dfa1d4f6810a1ccad5aa4457da04cb6b1964cff087147229130ff931d7350156007359b2cf972b4408884600296d78c297a5a8790dcaeac5377c566682eaa9d46b29dbbcfbf8f3b9ab8f415de00988885667f471bb338500b3dabbb5c35e144361ff74f894f376b75c292558fe07e4491beeceac3eb1dd66b3ad4f6c84ab9f4575d2743278362ca890edaf26435b2f108f9e336023800410bcecdfaf3982428fdbcb11bca6093044eacbd968bedafe040000ffff030069b828dae2060000 2018-10-01 12:09:53+00 customer@example.com expired 3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd CustomOrderId f
7 YZV1PQasUKEXdSkSV69XDL \x1f8b0800000000000003c455cb76db3610dde72b7cb86e65911429cbabca1463c792523d2c39759c05480c4584244003202bd5475fd6453e29bf50108455d9474dcfe9a64bdeb93373312f7efff3dbf3bbb3338b60ebf2ccfaed616dcfe648acc6e127bccc966b7ff06934b17e6a1842320e1f34cd159b5510c8d5d5c65f5e4c6e331ee164781f89dbe9d3d5eefd24a9974f5faffdedf8ea3ec4ad33e31878eb1c542a52f1ab01dad025009eb19cc43bc5e86a8c2309eac3f37da7e3f56cd7f55dcf751c6d23b46624863b523414db732fdc81ef38b636c2b624ca9930facadeb78d1d43c90491438c3908a19f33b85927f2b62f2807a7866c7ce1f5c49dbc76275e9a08e7a2ba6e65969ce12a961f68c278a13328efa67a8d2409c50844dcc49b2221815386e16c295146e846fb1b56a070c5a2559e1bb44c7782c42857688272012f30576f6cf477bb9daec1e28a73a0ba4c56b85a580add6b6d51b553f53ca54c5b3ea2e24d520d8705224d5a15b7690af05f608b8a32874ecc0aeb9819b08a4abe3b11e381942750f570792a6340e4a920a61bce3f9bec13a659cae84b92432554774748a223ba6a3927b52ecb523673b5d1f5db9655e48f86d1264daa4570efcf9fb83b4e96a3af24eb65e18ad4c32c49e3eb9b69b60ea1173d3c79ee7afe477f90f57a219b7bd35139b91d8f87b3b157afd7fe6cdb5fd90b77007312d1281d89fb4115ceed314e07c1282a368b9f3f978e48bf58a71511d0a3f8fc685ddd058fd6e5e3ff24efd1dabfac3b9295d644e177cb6c560c65abd9d80e152ed1ae002a1becf397767f21a92846510ec7636de0e9bf8d9de261c22196abc5a421a65296e2f2fcfc88772e534433b16355eb21b7efc1ac8bfe4e94b68f4c9244ad5623bad12679d5caa047861fa4504b994728ce84a909f01af88ae7c77cb5429482ec6028582792b12a45cbeb30de2ebe15f35d2999994ab396aacd878f1f9fbbf618b4059e824c5973469ff7075302b068bded03f6a6181afb4f874f3bef0fcb55304ad47f409db4f07066cd89ed75bb7d736253d2fc2c9a936692c1ab6141b56a7f3319c67a8344fa9a015b0914037edbbfbfe7086a336e6f47f08ee5c0116d2f67a7fb6eff17000000ffff03003a3b8e5ae2060000 2018-10-01 12:17:01+00 customer@example.com expired 3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd CustomOrderId f
8 3qtdXmycQwhWEPs91MExB2 \x1f8b0800000000000003c455cb769b4810dde72b7c5827b21002455e4502f2b0a48c1e96e538cea2a10bd106ba5177238bf8e8cb66319f34bf90a6c18aeca324e7cc6696dcba5575bb5efcfbf73f8fafcece0c828d8b33c3da487c9395e1ec215ef953d13727fe6ed8315e570c2119874f354dac97ae2b97c3b5b3783bbe4c7880a3c12a109793cdb07c3f8eb68bcdfd0767371aae7c5c3b338e81d7ce6ea122657f35401d3a07c0539692b0548cb6c63892a03e6cc7e9b4ecae69598e655b9d8eb611ba6524842b925514d3b6de5a7dc7b11d6d845d4e943361f499bd6737760c3913440e30e620847e8ef9b0bcf70a6bf6ddc3e3e48bdfe38e7f39ef4d2837931b424641af969973868b507ea211e399cea0bcabea559224641e88b08a37414202a70cc3d942a284d0b5f66f58aec2158b1669daa0795c0a12a254a1114a053cc15cbd51eb373bad76038605e740759d0c7f393714bad7e282a254053d254d5b3ea3ec45560dfb1922555e15b7ea0af077b043599e422b649971cc745941252f4fc4b825f90954bd5c9ecae812792a48d38eceaf4de609d33466f429c9a112aabd1e92e888ae7acec956976521abc15aebfaedf22270bc41b08ea362eeae9cd9865ba368e1dd93a49bf84bb21d24511c7ef83849ae7de806b71bdbba9e7deff5936ed767337be2e5e3cbd168301dd9dbeb6b67baeb2dcdb9d587190968107b62d52ffc9939c271dff5826c3d7ff335ef88f89b715a11013d8b8f77c6f0cabd332eeefe277977c6fe69df912cb4260a0f46b35a21e4b5e6c676a8708eca0ca8acb0afdfea0586a8a01805291ccf75034ffe34768a870987502ee7e38a184b998b8bf3f323deb98c114d44c98ada43eede83de97767d4122a5ed33932452bb5589aeb4495ed432e891e13729d456a6010a13d1d404f816f892a7c77cb54294826c61c8582b90a12a45cd6b315e6fbe11f23297ac99ca662d559b0f1fbfbf77f535a80b3c0119b3ea8e3eee0fa608605e7b9b07ec453134f69f2e9f76de1f962b6394a81f81ba69fee1ce3637b6db36cde6c6c6a4fa5b5437ad4906cf86056d55fbabc968ac1f91889f3360278162c02ffbf7738e60db8cdbcb11bc62297044ebd3d96abfdaff000000ffff0300fd61e9cae3060000 2018-10-01 12:24:16+00 customer@example.com expired 3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd CustomOrderId f
9 BtJGr7kt52QkZe8RdhKz42 \x1f8b0800000000000003c455c172da4810bde72b5c3a6f301208249f168462078cd780c115e21c469a161a4b9a9167465a888b2fcb219f945fc86824b3d8c566abf692a3de7b3dfda6a7bbf5e3dbf7e777676706c1c6c5993194e34bde4fa46dcd923538731c4fbe762de38f4a2124e3f051cb3a62b3f43cb91c6e7a0be77a9cf0004783fb408ca74fc3dd87eba85c3c3d5ef6b693e1bd8feb60c631f03ad82bd449d95f0d501f9d03e05b969270a7146d8d7124417dd83dc76a5996d9711dc771db3dcd115a3212c21dc92a8969779c8edb6ff72d4dc236272a9830fa9a771b1e43ce0491038c3908a1af73152d3ee55bfea9fb784978de21ebf286925537598ef12258737f52dbcc39c345283fd288f14c6750d155f52a4b12b21188b03a6f8a84044e1986b3854409a11b1ddfa83c852b152dd2b441f3782748885285462815f002737547eddf6cb79c7ebf6d9b96dd7061c139505d2ec35fce0d85eeb5c7a0d8a9ba9e72a8991b94bd49ae613f43a44a6fa4880b4168028efbe7a6025b21cb8c63a9c70a2af9eec4216b929f405505e4a9941e91a70e699ec5fa77ca3c41ddc68cbe243994423df308497424576fcf49a9ebb29055836d7401b77911f44683601347c5dcbbefcd9e7867122d468f24e926fe929483248ac3cbab69b2f2a11bac9fecce6af6b5ef26ddaecf66f674945f8f2793c1edc42e57abdeedb6bf34e71d176624a0413c12f76ee1cfcc098e5d6f14649bf9fbcfb925e22fc6694704744f3e3f18c33befc1b878f84df61e8cfdcbdc2359684f14fe369a110b21af3d37dca1c239da654065857dfe520f324405c52848e1b8bf1b78fa9f7da784987008e5727e5d2963297371717e0e5b94e52954ba7319239a881d2bea08b9fd007a70daf52a8994b91b2649a486ac725d9993bca87dd023e21729d478a6010a13d1140578097cc9d363bdf24d29c816868cb50219aa5ad4ba16e3f50a3042becb256bdab2194cf5ce878f5f2fbe7a2dd4159e828c59b5509ff7072a0298d7d1e6017b530c8dfdaf15a883f787e9ca1825ea8fa0969b7f58b8cdb2edb64dbb59b631a97e1bd5726b92c1ab6e41a57aeaaa351af60a89f8b502b6122806fcf6fdfe6924289b7e7bdb83772c058e68bd435bed77fb9f000000ffff030034f07389ec060000 2018-10-01 12:31:12+00 customer@gmail.com expired 3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd CustomOrderId f
10 VWB3riH9WuCv9NtSedaGE6 \x1f8b0800000000000003c455c1729b4810bde72b5c9cb3b21012029f222162c792bc9664a494631f0668c404984133032bc5a52fcb219f945fc83060457629deaabdec91f75e4fbfe9e96e7e7efff1f4eeec4cc3a17671a62d574383e12b7b5538a57d231610a24bd7d4de570a2e28834f4a66f0b5e738c21baecd8535b94e981f468395cfafa79be1eee3242a179baf97e6763c5cb9611d4c5908ac0e760a7952f67703d447e700e12d4d71b0938ab6c21812203f7aa6d569753aba615b9665b74dc56152521cc01dce2a89de332cc3eeebb6ae48d8e65806634a5ef056bbe143c829c76210860c3857d7e96fd6b6037d4f1457c10adb9fbd9bfb8531b059d17326cbcbd5106a9b39a36111884f24a22c5319647455bdca92806c043ca8ce9b222e80111ac2d942a00493b58a6f548ec4a58a1469daa079bce33840a944239472788699bca3f2afb75b56bfdfeee99d5ec30505634054b934d79b6b12dd2b8f7eb193753de5503137287b955cc16e8670955e4b11e31c93042cfbc3ba025b01cdb463a9430b22d8eec421f7383f81ca0a8853291d2c4e1dd23c4be7cf947e82ba8d29794e7228857ce61112e8482edf9ee152d56521aa065bab026ef3c23747037f1d47c5dc5999b30d33c6d162f41527ddc4f5703948a238b8bc9a264b17bafefda6672c67dffa76d2edba74d69b8ef2c9f5783cb81df7cae5d2bcddf63d7d6ed830c33ef1e3115fd9853bd3c7616c3b233f5bcffffa927778fca89d768441f5e4d38336bc731eb48b87ffc9de83b67f9e7b240ae589c03f5a336201e4b5e7863b543847bb0c88a8b02f8ff52043549010f9291cf777034fffb5efa430c40c02e1cd2795321622e717e7e7b045599e42a53b17312209dfd1a28e10db8fa006a75daf92489abba1024772c82ad79539c18ada073922de4821c733f55190f0a628c04a601e4b8ff5d23721205a2164b4e58b40d6a2d6b528ab578016b05d2e68d396cd60ca773e7cbcbdf8eab55057780a22a6d5427dda1fa808605e47eb07ec553114f69f56a00ade1fa62ba304cb3f825c6eee61e136cbb6dbd6cd66d9c6b8fa6d54cbad49062fba0595f2a9abd668d82bc4e3970ad80a202184afdfef772341d9f4dbeb1ebca3293044ea1dda6abfdbff020000ffff0300e1a26ae9ec060000 2018-10-01 12:33:11+00 customer@gmail.com expired 3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd CustomOrderId f
11 At4CDM5vfewV2WDHEPKRbt \x1f8b0800000000000003c4554d73a33810bdcfaf4871de758c0db6c9696d20c9c471c61fb15d35933908688c06908824183329ffb23dec4fdabfb092205e27e59daddacb1e79efb5faa9d5ddfcf9fb1f2f1f2e2e0c1c195717c65858ae37b3ab18be6f7a5befd69f4f9781307e510a2e28838f5ad6e7bbb5eb8af56437588deeef521644f1781bf0bbd9f3a4bebe8fabd5f3b79bc17e3ad9fa51134c5904ac09764b7952fea9059aa30b80684e331cd652d1d5184302e4873d18f53b3db33f1c398edd1d3a9ac3a4a23884479c2b8969f74756d71c0d4c4dc2bec0321853f286ef0d5b3e8282722cc651c480737d9db1fde361b901320bf8c4ab83bb38c2dc22d73dd7de06f5eac6228dcd82d1a80cc547125396eb0c325a554f5912907bc04375de0c71018cd0082e5602a598ec747cab72252e55a4ccb2162d929ae31065128d51c6e11566f28ecabf351a76468ee90c7bc3960a4bc680e86a19fe7a6948f4a02d06652dcb7acea0661e50fe2eb786fd1c6195ddc810e31c931446ce6f3b0576429a1ba752979644b0facc219f717106950510e752ba589c3ba47d95de3f53e6196a9e50f29ae4580af9ca1e12e8442e9f9ee14ad76525547fed7401f745190cbc71b04be272e96e078b67d69fc62bef1b4eadd45fe36a9cc64978733b4b373e58c1e767bbbf59fc183aa965f97461cfbce2fe6e3a1dcfa776b5d90ce6fbe1da5cf61d58e0800489c7b74ee92fcc699438ae17e4bbe5af5f8a1e4fbe1ae71d61d02df9f2644c1edd27e3eae97fb2f7641c5ec71e89527b22f0dd68272c84a2f1dc72c70a17a8ce8108857df9dacc31c425895090c1697bb7f0ec5ffb4e0a23cc2014ebe5bd52264214fceaf212f6282f3250ba4b912092f29a964d84d85f839e9b6eb3496269ee810a1ccb1953ae9539c1cac60739217e92424e6716a030e56d518055c0d62c3bd54bdf8480e84490d34e2042598b46d7a1acd90046c8ea42d0b62ddbc194ef7cfcf8f9de6bb64253e1198884aa7dfa72385231c0b289368fd8bb6268ec3f6d401d7c384e574e09963f04b9dbfce3be3deeda41bfddb509567f0db5dbda64f0a65b50259f5ab546cbde229ebc55c05e0089207aff7e7f3712546dbfbdefc1479a0143a459a19dee87c35f000000ffff0300011a2ad8eb060000 2018-10-01 13:51:01+00 customer@gmail.com expired 3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd CustomOrderId f

@ -133,7 +133,7 @@ namespace BTCPayServer.Tests
public async Task CanQueryDirectProviders()
{
// TODO: Check once in a while whether or not they are working again
string[] brokenShitcoinCasinos = { };
string[] brokenShitcoinCasinos = { "binance", "coinbasepro" };
var skipped = 0;
var factory = FastTests.CreateBTCPayRateFactory();
var directlySupported = factory.AvailableRateProviders.Where(s => s.Source == RateSource.Direct)
@ -272,7 +272,8 @@ namespace BTCPayServer.Tests
"https://www.bitpay.com", // not allowing to be hit from circleci
"https://support.bitpay.com",
"https://www.coingecko.com", // unhappy service
"https://www.wasabiwallet.io" // Banning US, CI unhappy
"https://www.wasabiwallet.io", // Banning US, CI unhappy
"https://fullynoded.app" // Sometimes DNS doesn't work
};
foreach (var match in regex.Matches(text).OfType<Match>())

@ -303,7 +303,7 @@ namespace BTCPayServer.Tests
// Set tolerance to 50%
var stores = user.GetController<UIStoresController>();
var response = await stores.GeneralSettings();
var response = await stores.GeneralSettings(user.StoreId);
var vm = Assert.IsType<GeneralSettingsViewModel>(Assert.IsType<ViewResult>(response).Model);
Assert.Equal(0.0, vm.PaymentTolerance);
vm.PaymentTolerance = 50.0;
@ -385,7 +385,7 @@ namespace BTCPayServer.Tests
await user.RegisterDerivationSchemeAsync("BTC");
await user.RegisterLightningNodeAsync("BTC", LightningConnectionType.CLightning);
await user.SetNetworkFeeMode(NetworkFeeMode.Never);
await user.ModifyOnchainPaymentSettings(p => p.SpeedPolicy = SpeedPolicy.HighSpeed);
await user.ModifyGeneralSettings(p => p.SpeedPolicy = SpeedPolicy.HighSpeed);
var invoice = await user.BitPay.CreateInvoiceAsync(new Invoice(0.0001m, "BTC"));
await tester.WaitForEvent<InvoiceNewPaymentDetailsEvent>(async () =>
{
@ -445,7 +445,7 @@ namespace BTCPayServer.Tests
var user = tester.NewAccount();
await user.GrantAccessAsync(true);
var storeController = user.GetController<UIStoresController>();
var storeResponse = await storeController.GeneralSettings();
var storeResponse = await storeController.GeneralSettings(user.StoreId);
Assert.IsType<ViewResult>(storeResponse);
Assert.IsType<ViewResult>(storeController.SetupLightningNode(user.StoreId, "BTC"));
@ -568,10 +568,10 @@ namespace BTCPayServer.Tests
using var tester = CreateServerTester();
await tester.StartAsync();
var acc = tester.NewAccount();
acc.GrantAccess();
await acc.GrantAccessAsync();
acc.RegisterDerivationScheme("BTC");
await acc.ModifyOnchainPaymentSettings(p => p.SpeedPolicy = SpeedPolicy.LowSpeed);
var invoice = acc.BitPay.CreateInvoice(new Invoice
await acc.ModifyGeneralSettings(p => p.SpeedPolicy = SpeedPolicy.LowSpeed);
var invoice = await acc.BitPay.CreateInvoiceAsync(new Invoice
{
Price = 5.0m,
Currency = "USD",
@ -1448,6 +1448,41 @@ namespace BTCPayServer.Tests
Assert.Contains("DOGE_X", rateVm.Script, StringComparison.OrdinalIgnoreCase);
}
[Fact]
[Trait("Integration", "Integration")]
public async Task CanTopUpPullPayment()
{
using var tester = CreateServerTester();
await tester.StartAsync();
var user = tester.NewAccount();
await user.GrantAccessAsync(true);
await user.RegisterDerivationSchemeAsync("BTC");
var client = await user.CreateClient();
var pp = await client.CreatePullPayment(user.StoreId, new()
{
Currency = "BTC",
Amount = 1.0m,
PayoutMethods = [ "BTC-CHAIN" ]
});
var controller = user.GetController<UIInvoiceController>();
var invoice = await controller.CreateInvoiceCoreRaw(new()
{
Amount = 0.5m,
Currency = "BTC",
}, controller.HttpContext.GetStoreData(), controller.Url.Link(null, null), [PullPaymentHostedService.GetInternalTag(pp.Id)]);
await client.MarkInvoiceStatus(user.StoreId, invoice.Id, new() { Status = InvoiceStatus.Settled });
await TestUtils.EventuallyAsync(async () =>
{
var payouts = await client.GetPayouts(pp.Id);
var payout = Assert.Single(payouts);
Assert.Equal("TOPUP", payout.PayoutMethodId);
Assert.Equal(invoice.Id, payout.Destination);
Assert.Equal(-0.5m, payout.OriginalAmount);
});
}
[Fact]
[Trait("Integration", "Integration")]
public async Task CanUseDefaultCurrency()
@ -1809,6 +1844,8 @@ namespace BTCPayServer.Tests
await user.GrantAccessAsync();
var user2 = tester.NewAccount();
await user2.GrantAccessAsync();
await user.RegisterDerivationSchemeAsync("BTC");
await user2.RegisterDerivationSchemeAsync("BTC");
var stores = user.GetController<UIStoresController>();
var apps = user.GetController<UIAppsController>();
var apps2 = user2.GetController<UIAppsController>();
@ -2091,7 +2128,7 @@ namespace BTCPayServer.Tests
Amount = 0.0001m,
Destination = (await tester.ExplorerNode.GetNewAddressAsync()).ToString(),
Approved = true,
PaymentMethod = "BTC"
PayoutMethodId = "BTC"
});
await user.AssertHasWebhookEvent(WebhookEventType.PayoutCreated, (WebhookPayoutEvent x)=> Assert.Equal(payout.Id, x.PayoutId));
await client.MarkPayout(user.StoreId, payout.Id, new MarkPayoutRequest(){ State = PayoutState.AwaitingApproval});
@ -2414,9 +2451,10 @@ namespace BTCPayServer.Tests
private static bool IsMapped(Invoice invoice, ApplicationDbContext ctx)
{
var h = BitcoinAddress.Create(invoice.BitcoinAddress, Network.RegTest).ScriptPubKey.Hash.ToString();
var pmi = PaymentTypes.CHAIN.GetPaymentMethodId("BTC");
return (ctx.AddressInvoices.Where(i => i.InvoiceDataId == invoice.Id).ToArrayAsync().GetAwaiter()
.GetResult())
.Where(i => i.GetAddress() == h).Any();
.Where(i => i.Address == h && i.PaymentMethodId == pmi.ToString()).Any();
}
@ -2581,7 +2619,7 @@ namespace BTCPayServer.Tests
}
var controller = tester.PayTester.GetController<UIStoresController>(user.UserId, user.StoreId);
var vm = await controller.GeneralSettings().AssertViewModelAsync<GeneralSettingsViewModel>();
var vm = await controller.GeneralSettings(user.StoreId).AssertViewModelAsync<GeneralSettingsViewModel>();
Assert.Equal(tester.PayTester.ServerUriWithIP + "LocalStorage/8f890691-87f9-4c65-80e5-3b7ffaa3551f-store.png", vm.LogoUrl);
Assert.Equal(tester.PayTester.ServerUriWithIP + "LocalStorage/2a51c49a-9d54-4013-80a2-3f6e69d08523-store.css", vm.CssUrl);
@ -2800,7 +2838,6 @@ namespace BTCPayServer.Tests
Assert.Equal("coingecko", b.PreferredExchange);
}
[Fact(Timeout = LongRunningTestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanDoInvoiceMigrations()
@ -3122,7 +3159,15 @@ namespace BTCPayServer.Tests
var invoiceId = GetInvoiceId(resp);
await acc.PayOnChain(invoiceId);
app = await client.CreatePointOfSaleApp(acc.StoreId, new PointOfSaleAppRequest
// Quick unrelated test on GetMonitoredInvoices
var invoiceRepo = tester.PayTester.GetService<InvoiceRepository>();
var monitored = Assert.Single(await invoiceRepo.GetMonitoredInvoices(PaymentMethodId.Parse("BTC-CHAIN")), i => i.Id == invoiceId);
Assert.Single(monitored.Payments);
monitored = Assert.Single(await invoiceRepo.GetMonitoredInvoices(PaymentMethodId.Parse("BTC-CHAIN"), true), i => i.Id == invoiceId);
Assert.Single(monitored.Payments);
//
app = await client.CreatePointOfSaleApp(acc.StoreId, new PointOfSaleAppRequest
{
AppName = "Cart",
DefaultView = PosViewType.Cart,
@ -3210,7 +3255,7 @@ namespace BTCPayServer.Tests
var inv = await client.CreateInvoice(acc.StoreId, new CreateInvoiceRequest() { Amount = 10m, Currency = "USD" });
await acc.PayInvoice(inv.Id);
await client.MarkInvoiceStatus(acc.StoreId, inv.Id, new MarkInvoiceStatusRequest() { Status = InvoiceStatus.Settled });
var refund = await client.RefundInvoice(acc.StoreId, inv.Id, new RefundInvoiceRequest() { RefundVariant = RefundVariant.Fiat, PaymentMethod = "BTC-CHAIN" });
var refund = await client.RefundInvoice(acc.StoreId, inv.Id, new RefundInvoiceRequest() { RefundVariant = RefundVariant.Fiat, PayoutMethodId = "BTC-CHAIN" });
async Task AssertData(string currency, decimal awaiting, decimal limit, decimal completed, bool fullyPaid)
{
@ -3229,7 +3274,7 @@ namespace BTCPayServer.Tests
}
await AssertData("USD", awaiting: 0.0m, limit: 10.0m, completed: 0.0m, fullyPaid: false);
var payout = await client.CreatePayout(refund.Id, new CreatePayoutRequest() { Destination = addr.ToString(), PaymentMethod = "BTC-CHAIN" });
var payout = await client.CreatePayout(refund.Id, new CreatePayoutRequest() { Destination = addr.ToString(), PayoutMethodId = "BTC-CHAIN" });
await AssertData("USD", awaiting: 10.0m, limit: 10.0m, completed: 0.0m, fullyPaid: false);
await client.ApprovePayout(acc.StoreId, payout.Id, new ApprovePayoutRequest());
await AssertData("USD", awaiting: 10.0m, limit: 10.0m, completed: 0.0m, fullyPaid: false);

@ -29,6 +29,11 @@ namespace BTCPayServer.Tests
BTCPayLogs.Configure(LoggerFactory);
}
public DatabaseTester CreateDBTester()
{
return new DatabaseTester(TestLogs, LoggerFactory);
}
public BTCPayNetworkProvider CreateNetworkProvider(ChainName chainName)
{
var conf = new ConfigurationRoot(new List<IConfigurationProvider>()
@ -58,9 +63,9 @@ namespace BTCPayServer.Tests
var bootstrap = Startup.CreateBootstrap(conf);
var services = new PluginServiceCollection(new ServiceCollection(), bootstrap);
var plugins = new List<BaseBTCPayServerPlugin>() { new BitcoinPlugin() };
#if ALTCOINS
plugins.Add(new BTCPayServer.Plugins.Altcoins.AltcoinsPlugin());
#endif
foreach (var p in plugins)
{
p.Execute(services);

@ -321,7 +321,7 @@ retry:
base.VisitTagHelper(node);
}
private string ToString(IntermediateNode? node)
private string ToString(IntermediateNode node)
{
return _txt.Substring(node.Source.Value.AbsoluteIndex, node.Source.Value.Length);
}

@ -10,14 +10,12 @@ services:
context: ..
dockerfile: BTCPayServer.Tests/Dockerfile
args:
CONFIGURATION_NAME: Altcoins-Release
CONFIGURATION_NAME: Release
environment:
TESTS_EXPERIMENTALV2_CONFIRM: "true"
TESTS_BTCRPCCONNECTION: server=http://bitcoind:43782;ceiwHEbqWI83:DwubwWsoo3
TESTS_LTCRPCCONNECTION: server=http://litecoind:43782;ceiwHEbqWI83:DwubwWsoo3
TESTS_BTCNBXPLORERURL: http://nbxplorer:32838/
TESTS_LTCNBXPLORERURL: http://nbxplorer:32838/
TESTS_DB: "Postgres"
TESTS_POSTGRES: User ID=postgres;Include Error Detail=true;Host=postgres;Port=5432;Database=btcpayserver
TESTS_EXPLORER_POSTGRES: User ID=postgres;Include Error Detail=true;Host=postgres;Port=5432;Database=nbxplorer
TESTS_HOSTNAME: tests

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