Compare commits

..

2 Commits

Author SHA1 Message Date
fde4b00d39 Revert file header change 2024-06-11 12:39:29 +02:00
d6970e6750 Fixing Fast Tests 2024-06-11 05:17:20 -05:00
55 changed files with 117 additions and 671 deletions

View File

@ -32,9 +32,9 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="HtmlSanitizer" Version="8.0.838" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.5" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.0" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.0-beta.2" />
</ItemGroup>
<ItemGroup>

View File

@ -109,7 +109,7 @@ namespace BTCPayServer.Client
{
var response = await _httpClient.SendAsync(
CreateHttpRequest(
$"api/v1/pull-payments/{HttpUtility.UrlEncode(pullPaymentId)}/lnurl",
$"/api/v1/pull-payments/{pullPaymentId}/lnurl",
method: HttpMethod.Get), cancellationToken);
return await HandleResponse<PullPaymentLNURL>(response);
}

View File

@ -1,5 +1,4 @@
using System;
using System.Globalization;
using NBitcoin.JsonConverters;
using Newtonsoft.Json;
@ -59,8 +58,6 @@ namespace BTCPayServer.Client.JsonConverters
return null;
return TimeSpan.Zero;
}
if (reader.TokenType == JsonToken.String && TimeSpan.TryParse(reader.Value?.ToString(), CultureInfo.InvariantCulture, out var res))
return res;
if (reader.TokenType != JsonToken.Integer)
throw new JsonObjectException("Invalid timespan, expected integer", reader);
return ToTimespan((long)reader.Value);

View File

@ -1,5 +1,3 @@
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Client.Models;
public class LightningAddressData
@ -8,5 +6,5 @@ public class LightningAddressData
public string CurrencyCode { get; set; }
public decimal? Min { get; set; }
public decimal? Max { get; set; }
public JObject InvoiceMetadata { get; set; }
}

View File

@ -1,8 +1,5 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Client.Models
{
@ -16,9 +13,7 @@ namespace BTCPayServer.Client.Models
public bool? Archived { get; set; }
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
public DateTimeOffset Created { get; set; }
[JsonExtensionData]
public IDictionary<string, JToken> AdditionalData { get; set; } = new Dictionary<string, JToken>();
}
}
public class PointOfSaleAppData : AppDataBase
{

View File

@ -3,11 +3,11 @@
<Import Project="../Build/Common.csproj" />
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.6">
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.7" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.5" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BTCPayServer.Abstractions\BTCPayServer.Abstractions.csproj" />

View File

@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
@ -110,22 +109,9 @@ namespace BTCPayServer.PluginPacker
private static Type[] GetAllExtensionTypesFromAssembly(Assembly assembly)
{
return GetLoadableTypes(assembly).Where(type =>
return assembly.GetTypes().Where(type =>
typeof(IBTCPayServerPlugin).IsAssignableFrom(type) &&
!type.IsAbstract).ToArray();
}
static Type[] GetLoadableTypes(Assembly assembly)
{
if (assembly == null)
throw new ArgumentNullException(nameof(assembly));
try
{
return assembly.GetTypes();
}
catch (ReflectionTypeLoadException e)
{
return e.Types.Where(t => t != null).ToArray();
}
}
}
}

View File

@ -4,7 +4,7 @@
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.10.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.9.2" />
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="6.0.0" />
<PackageReference Include="NBitcoin" Version="7.0.37" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />

View File

@ -93,7 +93,7 @@ namespace BTCPayServer.Services.Rates
if (ticker != null)
{
var pair = GetCurrencyPair(symbol);
if (pair is not null && ticker.Bid <= ticker.Ask)
if (pair is not null)
result.Add(new PairRate(pair, new BidAsk(ticker.Bid, ticker.Ask)));
}
}

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">
<Import Project="../Build/Common.csproj" />
<PropertyGroup>
<NoWarn>$(NoWarn),xUnit1031</NoWarn>
@ -23,8 +23,8 @@
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<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" Version="4.1.1" />
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="121.0.6167.8500" />
<PackageReference Include="xunit" Version="2.6.6" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.6">
<PrivateAssets>all</PrivateAssets>

View File

@ -47,6 +47,7 @@ using NBXplorer.DerivationStrategy;
using NBXplorer.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OpenQA.Selenium.DevTools.V100.DOMSnapshot;
using Xunit;
using Xunit.Abstractions;
using StoreData = BTCPayServer.Data.StoreData;

View File

@ -2160,17 +2160,6 @@ namespace BTCPayServer.Tests
Assert.Equal("BTC", pp.Currency);
Assert.True(pp.AutoApproveClaims);
Assert.Equal(0.79m, pp.Amount);
// If an invoice doesn't have payment because it has been marked as paid, we should still be able to refund it.
invoice = await client.CreateInvoice(user.StoreId, new CreateInvoiceRequest { Amount = 5000.0m, Currency = "USD" });
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.PaymentMethod,
RefundVariant = RefundVariant.CurrentRate
});
Assert.Equal(1.0m, refund.Amount);
Assert.Equal("BTC", refund.Currency);
}
[Fact(Timeout = TestTimeout)]
@ -3468,7 +3457,6 @@ namespace BTCPayServer.Tests
var store2 = (await adminClient.CreateStore(new CreateStoreRequest() { Name = "test2" })).Id;
var address1 = Guid.NewGuid().ToString("n").Substring(0, 8);
var address2 = Guid.NewGuid().ToString("n").Substring(0, 8);
var address3 = Guid.NewGuid().ToString("n").Substring(0, 8);
Assert.Empty(await adminClient.GetStoreLightningAddresses(store.Id));
Assert.Empty(await adminClient.GetStoreLightningAddresses(store2));
@ -3496,17 +3484,6 @@ namespace BTCPayServer.Tests
await adminClient.RemoveStoreLightningAddress(store2, address2);
Assert.Empty(await adminClient.GetStoreLightningAddresses(store2));
var store3 = (await adminClient.CreateStore(new CreateStoreRequest { Name = "test3" })).Id;
Assert.Empty(await adminClient.GetStoreLightningAddresses(store3));
var metadata = JObject.FromObject(new { test = 123 });
await adminClient.AddOrUpdateStoreLightningAddress(store3, address3, new LightningAddressData
{
InvoiceMetadata = metadata
});
var lnAddresses = await adminClient.GetStoreLightningAddresses(store3);
Assert.Single(lnAddresses);
Assert.Equal(metadata, lnAddresses[0].InvoiceMetadata);
}
[Fact(Timeout = 60 * 2 * 1000)]

View File

@ -2638,33 +2638,6 @@ namespace BTCPayServer.Tests
Assert.Contains("Total", sums[3].FindElement(By.CssSelector("th")).Text);
Assert.Contains("1 222,21 €", sums[3].FindElement(By.CssSelector("td")).Text);
// Receipt print
s.Driver.FindElement(By.Id("ReceiptLinkPrint")).Click();
windows = s.Driver.WindowHandles;
Assert.Equal(3, windows.Count);
s.Driver.SwitchTo().Window(windows[2]);
var paymentDetails = s.Driver.WaitForElement(By.CssSelector("#PaymentDetails table"));
items = paymentDetails.FindElements(By.CssSelector("tr.cart-data"));
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("1 234,00 €", items[0].FindElement(By.CssSelector(".val")).Text);
Assert.Contains("Manual entry 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);
Assert.Contains("Discount", sums[1].FindElement(By.CssSelector(".key")).Text);
Assert.Contains("10% = 123,46 €", sums[1].FindElement(By.CssSelector(".val")).Text);
Assert.Contains("Tip", sums[2].FindElement(By.CssSelector(".key")).Text);
Assert.Contains("10% = 111,11 €", sums[2].FindElement(By.CssSelector(".val")).Text);
Assert.Contains("Total", sums[3].FindElement(By.CssSelector(".key")).Text);
Assert.Contains("1 222,21 €", sums[3].FindElement(By.CssSelector(".val")).Text);
s.Driver.Close();
s.Driver.SwitchTo().Window(windows[1]);
s.Driver.Close();
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
// Once more with items
s.GoToUrl(editUrl);
s.Driver.FindElement(By.Id("ShowItems")).Click();
@ -2702,6 +2675,7 @@ namespace BTCPayServer.Tests
// Receipt
s.Driver.WaitForElement(By.Id("ReceiptLink")).Click();
cartData = s.Driver.FindElement(By.CssSelector("#CartData table"));
items = cartData.FindElements(By.CssSelector("tbody tr"));
sums = cartData.FindElements(By.CssSelector("tfoot tr"));
@ -2716,27 +2690,6 @@ namespace BTCPayServer.Tests
Assert.Contains("Total", sums[0].FindElement(By.CssSelector("th")).Text);
Assert.Contains("4,23 €", sums[0].FindElement(By.CssSelector("td")).Text);
// Receipt print
s.Driver.FindElement(By.Id("ReceiptLinkPrint")).Click();
windows = s.Driver.WindowHandles;
Assert.Equal(2, windows.Count);
s.Driver.SwitchTo().Window(windows[1]);
paymentDetails = s.Driver.WaitForElement(By.CssSelector("#PaymentDetails table"));
items = paymentDetails.FindElements(By.CssSelector("tr.cart-data"));
sums = paymentDetails.FindElements(By.CssSelector("tr.sums-data"));
Assert.Equal(3, items.Count);
Assert.Single(sums);
Assert.Contains("Black Tea", items[0].FindElement(By.CssSelector(".key")).Text);
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("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);
s.Driver.Close();
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
// Guest user can access recent transactions
s.GoToHome();
s.Logout();

View File

@ -2814,7 +2814,7 @@ namespace BTCPayServer.Tests
Password = "store@store.com",
Port = 1234,
Server = "store.com"
}), ""));
}), "", true));
Assert.Equal("store@store.com", (await Assert.IsType<StoreEmailSender>(await emailSenderFactory.GetEmailSender(acc.StoreId)).GetEmailSettings()).Login);
}

View File

@ -18,6 +18,7 @@ using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
using OpenQA.Selenium.DevTools.V100.Network;
using OpenQA.Selenium.Support.UI;
using Xunit;
using Xunit.Abstractions;

View File

@ -89,7 +89,7 @@ services:
- merchant_lnd
selenium:
image: selenium/standalone-chrome:125.0
image: selenium/standalone-chrome:101.0
extra_hosts:
- "tests:172.23.0.18"
expose:
@ -163,7 +163,7 @@ services:
- "bitcoin_datadir:/data"
customer_lightningd:
image: btcpayserver/lightning:v24.05
image: btcpayserver/lightning:v24.02.2
stop_signal: SIGKILL
restart: unless-stopped
environment:
@ -191,9 +191,8 @@ services:
- bitcoind
merchant_lightningd:
image: btcpayserver/lightning:v24.05
image: btcpayserver/lightning:v24.02.2
stop_signal: SIGKILL
restart: unless-stopped
environment:
EXPOSE_TCP: "true"
LIGHTNINGD_CHAIN: "btc"
@ -217,7 +216,6 @@ services:
- "merchant_lightningd_datadir:/root/.lightning"
depends_on:
- bitcoind
postgres:
image: postgres:13.4
environment:
@ -228,7 +226,7 @@ services:
- "5432"
merchant_lnd:
image: btcpayserver/lnd:v0.18.1-beta
image: btcpayserver/lnd:v0.18.0-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"
@ -263,7 +261,7 @@ services:
- bitcoind
customer_lnd:
image: btcpayserver/lnd:v0.18.1-beta
image: btcpayserver/lnd:v0.18.0-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"
@ -310,29 +308,27 @@ services:
- "tor_datadir:/home/tor/.tor"
- "torrcdir:/usr/local/etc/tor"
- "tor_servicesdir:/var/lib/tor/hidden_services"
monerod:
image: btcpayserver/monero:0.18.3.3
restart: unless-stopped
container_name: xmr_monerod
entrypoint: monerod --fixed-difficulty 200 --rpc-bind-ip=0.0.0.0 --confirm-external-bind --rpc-bind-port=18081 --block-notify="/bin/sh ./scripts/notifier.sh -k -X GET https://host.docker.internal:14142/monerolikedaemoncallback/block?cryptoCode=xmr&hash=%s" --testnet --no-igd --hide-my-port --offline --non-interactive
volumes:
- "monero_data:/home/monero/.bitmonero"
ports:
- "18081:18081"
image: btcpayserver/monero:0.18.2.2-5
restart: unless-stopped
container_name: xmr_monerod
entrypoint: sleep 999999
# entrypoint: monerod --fixed-difficulty 200 --rpc-bind-ip=0.0.0.0 --confirm-external-bind --rpc-bind-port=18081 --block-notify="/bin/sh ./scripts/notifier.sh -k -X GET https://host.docker.internal:14142/monerolikedaemoncallback/block?cryptoCode=xmr&hash=%s" --testnet --no-igd --hide-my-port --offline
volumes:
- "monero_data:/home/monero/.bitmonero"
ports:
- "18081:18081"
monero_wallet:
image: btcpayserver/monero:0.18.3.3
image: btcpayserver/monero:0.18.2.2-5
restart: unless-stopped
container_name: xmr_wallet_rpc
entrypoint: monero-wallet-rpc --testnet --rpc-bind-ip=0.0.0.0 --disable-rpc-login --confirm-external-bind --rpc-bind-port=18082 --non-interactive --trusted-daemon --daemon-address=monerod:18081 --wallet-dir=/wallet --tx-notify="/bin/sh ./scripts/notifier.sh -k -X GET https://host.docker.internal:14142/monerolikedaemoncallback/tx?cryptoCode=xmr&hash=%s"
entrypoint: monero-wallet-rpc --testnet --rpc-bind-ip=0.0.0.0 --disable-rpc-login --confirm-external-bind --rpc-bind-port=18082 --non-interactive --trusted-daemon --daemon-address=monerod:18081 --wallet-file=/wallet/wallet.keys --password-file=/wallet/password --tx-notify="/bin/sh ./scripts/notifier.sh -k -X GET https://host.docker.internal:14142/monerolikedaemoncallback/tx?cryptoCode=xmr&hash=%s"
ports:
- "18082:18082"
volumes:
- "./monero_wallet:/wallet"
depends_on:
- monerod
- monerod
litecoind:
restart: unless-stopped
image: btcpayserver/litecoin:0.18.1

View File

@ -86,7 +86,7 @@ services:
- merchant_lnd
selenium:
image: selenium/standalone-chrome:125.0
image: selenium/standalone-chrome:101.0
extra_hosts:
- "tests:172.23.0.18"
expose:
@ -149,7 +149,7 @@ services:
- "bitcoin_datadir:/data"
customer_lightningd:
image: btcpayserver/lightning:v24.05
image: btcpayserver/lightning:v24.02.2
stop_signal: SIGKILL
restart: unless-stopped
environment:
@ -177,9 +177,8 @@ services:
- bitcoind
merchant_lightningd:
image: btcpayserver/lightning:v24.05
image: btcpayserver/lightning:v24.02.2
stop_signal: SIGKILL
restart: unless-stopped
environment:
EXPOSE_TCP: "true"
LIGHTNINGD_CHAIN: "btc"
@ -214,7 +213,7 @@ services:
- "5432"
merchant_lnd:
image: btcpayserver/lnd:v0.18.1-beta
image: btcpayserver/lnd:v0.18.0-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"
@ -251,7 +250,7 @@ services:
- bitcoind
customer_lnd:
image: btcpayserver/lnd:v0.18.1-beta
image: btcpayserver/lnd:v0.18.0-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"

View File

@ -1,199 +0,0 @@
#!/bin/bash
USERHOST="btcpay.local"
BASE="https://localhost:14142"
API_BASE="$BASE/api/v1"
PASSWORD="rockstar"
# Ensure we are in the script directory
cd "$(dirname "${BASH_SOURCE}")"
# Create admin user
admin_id=$(curl -s -k -X POST -H 'Content-Type: application/json' \
-d "{'email': 'admin@$USERHOST', 'password': '$PASSWORD', 'isAdministrator': true }" \
"$API_BASE/users" | jq -r '.id')
printf "Admin ID: %s\n" "$admin_id"
# Create unlimited access API key
admin_api_key=$(curl -s -k -X POST -H 'Content-Type: application/json' \
-d "{'permissions': ['unrestricted'], 'label': 'Unrestricted' }" \
--user "admin@$USERHOST:$PASSWORD" \
"$API_BASE/api-keys" | jq -r '.apiKey')
printf "Admin API Key: %s\n" "$admin_api_key"
printf "\n"
# Create Store Owner
owner_id=$(curl -s -k -X POST -H 'Content-Type: application/json' \
-d "{'email': 'owner@$USERHOST', 'password': '$PASSWORD', 'isAdministrator': false }" \
-H "Authorization: token $admin_api_key" \
"$API_BASE/users" | jq -r '.id')
printf "Store Owner ID: %s\n" "$owner_id"
# Create Store Manager
manager_id=$(curl -s -k -X POST -H 'Content-Type: application/json' \
-d "{'email': 'manager@$USERHOST', 'password': '$PASSWORD', 'isAdministrator': false }" \
-H "Authorization: token $admin_api_key" \
"$API_BASE/users" | jq -r '.id')
printf "Store Manager ID: %s\n" "$manager_id"
# Create Store Employee
employee_id=$(curl -s -k -X POST -H 'Content-Type: application/json' \
-d "{'email': 'employee@$USERHOST', 'password': '$PASSWORD', 'isAdministrator': false }" \
-H "Authorization: token $admin_api_key" \
"$API_BASE/users" | jq -r '.id')
printf "Store Employee ID: %s\n" "$employee_id"
printf "\n"
# Create Satoshis Steaks store
res=$(curl -s -k -X POST -H 'Content-Type: application/json' \
-d "{'name': 'Satoshis Steaks', 'checkoutType': 'V2', 'lightningAmountInSatoshi': true, 'onChainWithLnInvoiceFallback': true, 'playSoundOnPayment': true, 'defaultCurrency': 'EUR' }" \
-H "Authorization: token $admin_api_key" \
"$API_BASE/stores")
store_id_satoshis_steaks=$( echo $res | jq -r '.id')
if [ -z "${store_id_satoshis_steaks}" ]; then
printf "Error creating Satoshis Steaks store: %s\n" "$res"
exit 1
fi
printf "Satoshis Steaks Store ID: %s\n" "$store_id_satoshis_steaks"
# Create Hot Wallet for Satoshis Steaks store
wallet_enabled_satoshis_steaks=$(curl -s -k -X PUT -H 'Content-Type: application/json' \
-d "{'enabled': true, 'config': 'tpubDC2mCtL7EPhey3qRgHXmKQRraxXgiuSTkHdJbDW22xLK1YMXy8jdEq7jx2UN5z1wU5xBWWZdSpAobG1bbZBTR4f8R3AjL31EzoexpngKUXM' }" \
-H "Authorization: token $admin_api_key" \
"$API_BASE/stores/$store_id_satoshis_steaks/payment-methods/BTC-CHAIN")
# Create Internal Node connection for Satoshis Steaks store
ln_enabled_satoshis_steaks=$(curl -s -k -X PUT -H 'Content-Type: application/json' \
-d "{'enabled': true, 'config': { 'connectionString': 'Internal Node' } }" \
-H "Authorization: token $admin_api_key" \
"$API_BASE/stores/$store_id_satoshis_steaks/payment-methods/BTC-LN")
# LNURL settings
curl -s -k -X PUT -H 'Content-Type: application/json' \
-d "{'enabled': true, 'config': { 'lud12Enabled': true, 'useBech32Scheme': true } }" \
-H "Authorization: token $admin_api_key" \
"$API_BASE/stores/$store_id_satoshis_steaks/payment-methods/BTC-LNURL" >/dev/null 2>&1
# Fund Satoshis Steaks wallet
btcaddress_satoshis_steaks=$(curl -s -k -X GET -H 'Content-Type: application/json' \
-H "Authorization: token $admin_api_key" \
"$API_BASE/stores/$store_id_satoshis_steaks/payment-methods/onchain/BTC/wallet/address" | jq -r '.address')
./docker-bitcoin-cli.sh sendtoaddress "$btcaddress_satoshis_steaks" 6.15 >/dev/null 2>&1
printf "\n"
# Add store users to Satoshis Steaks store
curl -s -k -X POST -H 'Content-Type: application/json' \
-d "{'userId': '$owner_id', 'role': 'Owner' }" \
-H "Authorization: token $admin_api_key" \
"$API_BASE/stores/$store_id_satoshis_steaks/users" >/dev/null 2>&1
curl -s -k -X POST -H 'Content-Type: application/json' \
-d "{'userId': '$manager_id', 'role': 'Manager' }" \
-H "Authorization: token $admin_api_key" \
"$API_BASE/stores/$store_id_satoshis_steaks/users" >/dev/null 2>&1
curl -s -k -X POST -H 'Content-Type: application/json' \
-d "{'userId': '$employee_id', 'role': 'Employee' }" \
-H "Authorization: token $admin_api_key" \
"$API_BASE/stores/$store_id_satoshis_steaks/users" >/dev/null 2>&1
# Create Nakamoto Nuggets store
store_id_nakamoto_nuggets=$(curl -s -k -X POST -H 'Content-Type: application/json' \
-d "{'name': 'Nakamoto Nuggets', 'checkoutType': 'V2', 'lightningAmountInSatoshi': true, 'onChainWithLnInvoiceFallback': true, 'playSoundOnPayment': true }" \
-H "Authorization: token $admin_api_key" \
"$API_BASE/stores" | jq -r '.id')
printf "Nakamoto Nuggets Store ID: %s\n" "$store_id_nakamoto_nuggets"
# Create Hot Wallet for Nakamoto Nuggets store
# Seed: "resist camera spread better amazing cliff giraffe duty betray throw twelve father"
wallet_enabled_nakamoto_nuggets=$(curl -s -k -X PUT -H 'Content-Type: application/json' \
-d "{'enabled': true, 'config': 'tpubDD79XF4pzhmPSJ9AyUay9YbXAeD1c6nkUqC32pnKARJH6Ja5hGUfGc76V82ahXpsKqN6UcSGXMkzR34aZq4W23C6DAdZFaVrzWqzj24F8BC' }" \
-H "Authorization: token $admin_api_key" \
"$API_BASE/stores/$store_id_nakamoto_nuggets/payment-methods/BTC-CHAIN")
# Connect Nakamoto Nuggets with Merchant LND Lightning node
curl -s -k -X PUT -H 'Content-Type: application/json' \
-d "{'enabled': true, 'config': { 'connectionString': 'type=lnd-rest;server=http://lnd:lnd@127.0.0.1:35531/;allowinsecure=true' }}" \
-H "Authorization: token $admin_api_key" \
"$API_BASE/stores/$store_id_nakamoto_nuggets/payment-methods/BTC-LN" >/dev/null 2>&1
# LNURL settings
curl -s -k -X PUT -H 'Content-Type: application/json' \
-d "{'enabled': true, 'config': { 'lud12Enabled': true, 'useBech32Scheme': true } }" \
-H "Authorization: token $admin_api_key" \
"$API_BASE/stores/$store_id_nakamoto_nuggets/payment-methods/BTC-LNURL" >/dev/null 2>&1
# Add store users to Nakamoto Nuggets store
curl -s -k -X POST -H 'Content-Type: application/json' \
-d "{'userId': '$owner_id', 'role': 'Owner' }" \
-H "Authorization: token $admin_api_key" \
"$API_BASE/stores/$store_id_nakamoto_nuggets/users" >/dev/null 2>&1
curl -s -k -X POST -H 'Content-Type: application/json' \
-d "{'userId': '$manager_id', 'role': 'Manager' }" \
-H "Authorization: token $admin_api_key" \
"$API_BASE/stores/$store_id_nakamoto_nuggets/users" >/dev/null 2>&1
curl -s -k -X POST -H 'Content-Type: application/json' \
-d "{'userId': '$employee_id', 'role': 'Employee' }" \
-H "Authorization: token $admin_api_key" \
"$API_BASE/stores/$store_id_nakamoto_nuggets/users" >/dev/null 2>&1
# Create Nakamoto Nuggets keypad app
keypad_app_id_nakamoto_nuggets=$(curl -s -k -X POST -H 'Content-Type: application/json' \
-d "{'appName': 'Keypad', 'title': 'Keypad', 'defaultView': 'light', 'currency': 'SATS' }" \
-H "Authorization: token $admin_api_key" \
"$API_BASE/stores/{$store_id_nakamoto_nuggets}/apps/pos" | jq -r '.id')
printf "Nakamoto Nuggets Keypad POS ID: %s\n" "$keypad_app_id_nakamoto_nuggets"
# Create Nakamoto Nuggets cart app
cart_app_id_nakamoto_nuggets=$(curl -s -k -X POST -H 'Content-Type: application/json' \
-d "{'appName': 'Cart', 'title': 'Cart', 'defaultView': 'cart', 'template': '[{\"id\":\"birell beer\",\"image\":\"https://i.imgur.com/r8N6rTU.png\",\"priceType\":\"Fixed\",\"price\":\"20\",\"title\":\"Birell Beer\",\"disabled\":false},{\"id\":\"flavoured birell beer\",\"image\":\"https://i.imgur.com/de43iUd.png\",\"priceType\":\"Fixed\",\"price\":\"20\",\"title\":\"Flavoured Birell Beer\",\"disabled\":false},{\"id\":\"wostok\",\"image\":\"https://i.imgur.com/gP6zqub.png\",\"priceType\":\"Fixed\",\"price\":\"25\",\"title\":\"Wostok\",\"disabled\":false},{\"id\":\"pilsner beer\",\"image\":\"https://i.imgur.com/M4EEaEP.png\",\"priceType\":\"Fixed\",\"price\":\"30\",\"title\":\"Pilsner Beer\",\"disabled\":false},{\"id\":\"club mate\",\"image\":\"https://i.imgur.com/H9p9Xwc.png\",\"priceType\":\"Fixed\",\"price\":\"35\",\"title\":\"Club Mate\",\"disabled\":false},{\"id\":\"seicha / selo / koka\",\"image\":\"https://i.imgur.com/ReW3RKe.png\",\"priceType\":\"Fixed\",\"price\":\"35\",\"title\":\"Seicha / Selo / Koka\",\"disabled\":false},{\"id\":\"limonada z kopanic\",\"image\":\"https://i.imgur.com/2Xb35Zs.png\",\"priceType\":\"Fixed\",\"price\":\"40\",\"title\":\"Limonada z Kopanic\",\"disabled\":false},{\"id\":\"mellow drink\",\"image\":\"https://i.imgur.com/ilDUWiP.png\",\"priceType\":\"Fixed\",\"price\":\"40\",\"title\":\"Mellow Drink\",\"disabled\":false},{\"id\":\"bacilli drink\",\"image\":\"https://i.imgur.com/3BsCLgG.png\",\"priceType\":\"Fixed\",\"price\":\"40\",\"title\":\"Bacilli Drink\",\"disabled\":false},{\"description\":\"\",\"id\":\"vincentka\",\"image\":\"https://i.imgur.com/99reAEg.png\",\"priceType\":\"Fixed\",\"price\":\"20\",\"title\":\"Vincentka\",\"disabled\":false,\"index\":\"-1\"},{\"id\":\"kinder bar\",\"image\":\"https://i.imgur.com/va9i6SQ.png\",\"priceType\":\"Fixed\",\"price\":\"20\",\"title\":\"Kinder bar\",\"disabled\":false},{\"id\":\"nutrend bar\",\"image\":\"https://i.imgur.com/zzdIup0.png\",\"priceType\":\"Fixed\",\"price\":\"15\",\"title\":\"Nutrend bar\",\"disabled\":false},{\"id\":\"yoghurt\",\"image\":\"https://i.imgur.com/biP4Dr8.png\",\"priceType\":\"Fixed\",\"price\":\"20\",\"title\":\"Yoghurt\",\"disabled\":false},{\"id\":\"mini magnum\",\"image\":\"https://i.imgur.com/tveN4Aa.png\",\"priceType\":\"Fixed\",\"price\":\"35\",\"title\":\"Mini Magnum\",\"disabled\":false},{\"description\":\"\",\"id\":\"nanuk do:pusy\",\"image\":\"https://i.imgur.com/EzZN6lV.png\",\"priceType\":\"Fixed\",\"price\":\"30\",\"title\":\"Nanuk DO:PUSY\",\"disabled\":false,\"index\":\"-1\"},{\"id\":\"alpro dessert\",\"image\":\"https://i.imgur.com/L0MHkcs.png\",\"priceType\":\"Fixed\",\"price\":\"30\",\"title\":\"Alpro dessert\",\"disabled\":false},{\"id\":\"mixitka bar\",\"image\":\"https://i.imgur.com/gHuTGK3.png\",\"priceType\":\"Fixed\",\"price\":\"30\",\"title\":\"Mixitka bar\",\"disabled\":false},{\"id\":\"instatni polivka\",\"image\":\"https://cdn.rohlik.cz/images/grocery/products/722313/722313-1598298944.jpg\",\"priceType\":\"Fixed\",\"price\":\"15\",\"title\":\"Instatni polivka\",\"disabled\":false},{\"id\":\"m&amp;s instatni polivka\",\"image\":\"https://i.imgur.com/Y8LCJbG.png\",\"priceType\":\"Fixed\",\"price\":\"60\",\"title\":\"M&amp;S instatni polivka\",\"disabled\":false}]' }" \
-H "Authorization: token $admin_api_key" \
"$API_BASE/stores/{$store_id_nakamoto_nuggets}/apps/pos" | jq -r '.id')
printf "Nakamoto Nuggets Cart POS ID: %s\n" "$cart_app_id_nakamoto_nuggets"
# Fund Nakamoto Nuggets wallet
btcaddress_nakamoto_nuggets=$(curl -s -k -X GET -H 'Content-Type: application/json' \
-H "Authorization: token $admin_api_key" \
"$API_BASE/stores/$store_id_nakamoto_nuggets/payment-methods/onchain/BTC/wallet/address" | jq -r '.address')
./docker-bitcoin-cli.sh sendtoaddress "$btcaddress_nakamoto_nuggets" 6.15 >/dev/null 2>&1
printf "\n"
# Create External Lightning based store
store_id_externalln=$(curl -s -k -X POST -H 'Content-Type: application/json' \
-d "{'name': 'External Lightning (LND)', 'checkoutType': 'V2', 'lightningAmountInSatoshi': true, 'onChainWithLnInvoiceFallback': true }" \
-H "Authorization: token $admin_api_key" \
"$API_BASE/stores" | jq -r '.id')
printf "External Lightning Store ID: %s\n" "$store_id_externalln"
# Connect External Lightning based store with Customer LND Lightning node
curl -s -k -X PUT -H 'Content-Type: application/json' \
-d "{'enabled': true, 'config': { 'connectionString': 'type=lnd-rest;server=http://lnd:lnd@127.0.0.1:35532/;allowinsecure=true' } }" \
-H "Authorization: token $admin_api_key" \
"$API_BASE/stores/$store_id_externalln/payment-methods/BTC-LN" >/dev/null 2>&1
# LNURL settings
curl -s -k -X PUT -H 'Content-Type: application/json' \
-d "{'enabled': true, 'config': { 'lud12Enabled': true, 'useBech32Scheme': true } }" \
-H "Authorization: token $admin_api_key" \
"$API_BASE/stores/$store_id_externalln/payment-methods/BTC-LNURL" >/dev/null 2>&1
printf "\n"
# Mine some blocks
./docker-bitcoin-generate.sh 5 >/dev/null 2>&1

View File

@ -77,8 +77,8 @@
<PackageReference Include="TwentyTwenty.Storage.Azure" Version="2.12.1" />
<PackageReference Include="TwentyTwenty.Storage.Google" Version="2.12.1" />
<PackageReference Include="TwentyTwenty.Storage.Local" Version="2.12.1" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="8.0.6" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.7" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="8.0.5" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.5" />
</ItemGroup>
<ItemGroup>

View File

@ -4,23 +4,23 @@
var isTruncated = !string.IsNullOrEmpty(Model.Start) && !string.IsNullOrEmpty(Model.End);
@if (Model.Copy) classes += " truncate-center--copy";
@if (Model.Elastic) classes += " truncate-center--elastic";
var prefix = Model.IsVue ? ":" : "";
}
<span class="truncate-center @classes" id="@Model.Id" data-text="@Model.Text">
<span class="truncate-center @classes"@(!string.IsNullOrEmpty(Model.Id) ? $"id={Model.Id}" : null) data-text=@Safe.Json(Model.Text)>
@if (Model.IsVue)
{
<span class="truncate-center-truncated" data-bs-toggle="tooltip" :title="@Model.Text">
<span class="truncate-center-truncated" data-bs-toggle="tooltip" :title=@Safe.Json(Model.Text)>
@if (Model.Elastic)
{
<span class="truncate-center-start" v-text="@Model.Text"></span>
<span class="truncate-center-start" v-text=@Safe.Json(Model.Text)></span>
}
else
{
<span class="truncate-center-start" v-text="@(Model.Text).length > 2 * @(Model.Padding) ? (@(Model.Text).slice(0, @(Model.Padding)) + '…') : @(Model.Text)"></span>
<span class="truncate-center-start" v-text=@Safe.Json($"{Model.Text}.slice(0, {Model.Padding})")></span>
<span>…</span>
}
<span class="truncate-center-end" v-text="@(Model.Text).slice(-@(Model.Padding))" v-if="@(Model.Text).length > 2 * @(Model.Padding)"></span>
<span class="truncate-center-end" v-text=@Safe.Json($"{Model.Text}.slice(-{Model.Padding})")></span>
</span>
<span class="truncate-center-text" v-text="@Model.Text"></span>
<span class="truncate-center-text" v-text=@Safe.Json(Model.Text)></span>
}
else
{
@ -35,13 +35,13 @@
}
@if (Model.Copy)
{
<button type="button" class="btn btn-link p-0" @(prefix)data-clipboard="@Model.Text">
<button type="button" class="btn btn-link p-0" @(Model.IsVue ? ":" : string.Empty)data-clipboard=@Safe.Json(Model.Text)>
<vc:icon symbol="copy" />
</button>
}
@if (!string.IsNullOrEmpty(Model.Link))
{
<a @(prefix)href="@Model.Link" rel="noreferrer noopener" target="_blank">
<a @(Model.IsVue ? ":" : "")href="@Model.Link" rel="noreferrer noopener" target="_blank">
<vc:icon symbol="info" />
</a>
}

View File

@ -213,7 +213,6 @@ namespace BTCPayServer.Controllers.Greenfield
}
[HttpDelete("~/api/v1/apps/{appId}")]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public async Task<IActionResult> DeleteApp(string appId)
{
var app = await _appService.GetApp(appId, null, includeArchived: true);

View File

@ -401,14 +401,6 @@ namespace BTCPayServer.Controllers.Greenfield
var accounting = invoicePaymentMethod.Calculate();
var cryptoPaid = accounting.Paid;
var dueAmount = accounting.TotalDue;
// If no payment, but settled and marked, assume it has been fully paid
if (cryptoPaid is 0 && invoice is { Status: InvoiceStatusLegacy.Confirmed or InvoiceStatusLegacy.Complete, ExceptionStatus: InvoiceExceptionStatus.Marked })
{
cryptoPaid = accounting.TotalDue;
dueAmount = 0;
}
var cdCurrency = _currencyNameTable.GetCurrencyData(invoice.Currency, true);
var paidCurrency = Math.Round(cryptoPaid * invoicePaymentMethod.Rate, cdCurrency.Divisibility);
var rateResult = await _rateProvider.FetchRate(
@ -476,6 +468,7 @@ namespace BTCPayServer.Controllers.Greenfield
return this.CreateValidationError(ModelState);
}
var dueAmount = accounting.TotalDue;
createPullPayment.Currency = cryptoCode;
createPullPayment.Amount = Math.Round(paidAmount - dueAmount, appliedDivisibility);
createPullPayment.AutoApproveClaims = true;

View File

@ -1,5 +1,4 @@
#nullable enable
using System;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
@ -9,8 +8,6 @@ using BTCPayServer.Data;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using AuthenticationSchemes = BTCPayServer.Abstractions.Constants.AuthenticationSchemes;
using LightningAddressData = BTCPayServer.Client.Models.LightningAddressData;
@ -34,13 +31,12 @@ namespace BTCPayServer.Controllers.Greenfield
var blob = data.GetBlob();
if (blob is null)
return new LightningAddressData();
return new LightningAddressData
return new LightningAddressData()
{
Username = data.Username,
Max = blob.Max,
Min = blob.Min,
CurrencyCode = blob.CurrencyCode,
InvoiceMetadata = blob.InvoiceMetadata
CurrencyCode = blob.CurrencyCode
};
}
@ -87,17 +83,16 @@ namespace BTCPayServer.Controllers.Greenfield
ModelState.AddModelError(nameof(data.Min), "Minimum must be greater than 0 if provided.");
return this.CreateValidationError(ModelState);
}
if (await _lightningAddressService.Set(new Data.LightningAddressData
if (await _lightningAddressService.Set(new Data.LightningAddressData()
{
StoreDataId = storeId,
Username = username
}.SetBlob(new LightningAddressDataBlob
}.SetBlob(new LightningAddressDataBlob()
{
Max = data.Max,
Min = data.Min,
CurrencyCode = data.CurrencyCode,
InvoiceMetadata = data.InvoiceMetadata
CurrencyCode = data.CurrencyCode
})))
{
return await GetStoreLightningAddress(storeId, username);

View File

@ -213,7 +213,7 @@ namespace BTCPayServer.Controllers
{
InvoiceId = i.Id,
OrderId = i.Metadata?.OrderId,
RedirectUrl = i.RedirectURL?.AbsoluteUri ?? i.Metadata?.OrderUrl,
OrderUrl = i.Metadata?.OrderUrl,
Status = i.Status.ToModernStatus(),
Currency = i.Currency,
Timestamp = i.InvoiceTime,
@ -249,12 +249,10 @@ namespace BTCPayServer.Controllers
receiptData.Remove(key);
}
}
// assign the rest to additional data and remove empty values
// assign the rest to additional data
if (receiptData.Any())
{
vm.AdditionalData = receiptData
.Where(x => !string.IsNullOrEmpty(x.Value.ToString()))
.ToDictionary(x => x.Key, x => x.Value);
vm.AdditionalData = receiptData;
}
}
@ -368,11 +366,6 @@ namespace BTCPayServer.Controllers
accounting = paymentMethod.Calculate();
cryptoPaid = accounting.Paid;
dueAmount = accounting.TotalDue;
if (cryptoPaid is 0 && invoice is { Status: InvoiceStatusLegacy.Confirmed or InvoiceStatusLegacy.Complete, ExceptionStatus: InvoiceExceptionStatus.Marked })
{
cryptoPaid = accounting.TotalDue;
dueAmount = 0;
}
paidAmount = cryptoPaid.RoundToSignificant(appliedDivisibility);
}

View File

@ -90,14 +90,11 @@ namespace BTCPayServer
_pluginHookService = pluginHookService;
_invoiceActivator = invoiceActivator;
}
[EnableCors(CorsPolicies.All)]
[HttpGet("withdraw/pp/{pullPaymentId}")]
public Task<IActionResult> GetLNURLForPullPayment(string cryptoCode, string pullPaymentId, [FromQuery] string pr, CancellationToken cancellationToken)
{
return GetLNURLForPullPayment(cryptoCode, pullPaymentId, pr, pullPaymentId, cancellationToken);
}
[NonAction]
internal async Task<IActionResult> GetLNURLForPullPayment(string cryptoCode, string pullPaymentId, string pr, string k1, CancellationToken cancellationToken)
{

View File

@ -195,11 +195,10 @@ namespace BTCPayServer.Controllers
if (store == null)
return NotFound();
ViewBag.UseCustomSMTP = useCustomSMTP;
model.FallbackSettings = await _emailSenderFactory.GetEmailSender(store.Id) is StoreEmailSender { FallbackSender: not null } storeSender
? await storeSender.FallbackSender.GetEmailSettings()
: null;
if (model.FallbackSettings is null) useCustomSMTP = true;
ViewBag.UseCustomSMTP = useCustomSMTP;
if (useCustomSMTP)
{
model.Settings.Validate("Settings.", ModelState);

View File

@ -73,11 +73,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike
{
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(30));
using var t = CancellationTokenSource.CreateLinkedTokenSource(timeout.Token, cancellationToken);
var rawInfo = await LNURL.LNURL.FetchInformation(lnurl, CreateClient(lnurl), t.Token);
if(rawInfo is null)
return (null, "The LNURL / Lightning Address provided was not online.");
if(rawInfo is not LNURLPayRequest info)
return (null, "The LNURL was not a valid LNURL Pay request.");
var info = (LNURLPayRequest)(await LNURL.LNURL.FetchInformation(lnurl, CreateClient(lnurl), t.Token));
lnurlTag = info.Tag;
}
@ -147,27 +143,9 @@ namespace BTCPayServer.Data.Payouts.LightningLike
return Task.CompletedTask;
}
public async Task<decimal> GetMinimumPayoutAmount(PaymentMethodId paymentMethod, IClaimDestination claimDestination)
public Task<decimal> GetMinimumPayoutAmount(PaymentMethodId paymentMethodId, IClaimDestination claimDestination)
{
if(claimDestination is LNURLPayClaimDestinaton lnurlPayClaimDestinaton)
{
try
{
var lnurl = lnurlPayClaimDestinaton.LNURL.IsValidEmail()
? LNURL.LNURL.ExtractUriFromInternetIdentifier(lnurlPayClaimDestinaton.LNURL)
: LNURL.LNURL.Parse(lnurlPayClaimDestinaton.LNURL, out var lnurlTag);
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(30));
var rawInfo = await LNURL.LNURL.FetchInformation(lnurl, CreateClient(lnurl), timeout.Token);
if (rawInfo is LNURLPayRequest info)
return info.MinSendable.ToDecimal(LightMoneyUnit.BTC);
}
catch
{
// ignored
}
}
return Money.Satoshis(1).ToDecimal(MoneyUnit.BTC);
return Task.FromResult(Money.Satoshis(1).ToDecimal(MoneyUnit.BTC));
}
public Dictionary<PayoutState, List<(string Action, string Text)>> GetPayoutSpecificActions()

View File

@ -244,15 +244,13 @@ namespace BTCPayServer.Data.Payouts.LightningLike
var lm = new LightMoney(blob.CryptoAmount.Value, LightMoneyUnit.BTC);
if (lm > lnurlInfo.MaxSendable || lm < lnurlInfo.MinSendable)
{
payoutData.State = PayoutState.Cancelled;
return (null, new ResultVM
{
PayoutId = payoutData.Id,
Result = PayResult.Error,
Destination = blob.Destination,
Message =
$"The LNURL provided would not generate an invoice of {lm.ToDecimal(LightMoneyUnit.Satoshi)} sats"
$"The LNURL provided would not generate an invoice of {lm.MilliSatoshi}msats"
});
}

View File

@ -304,7 +304,6 @@ namespace BTCPayServer.Hosting
});
services.TryAddSingleton<BTCPayNetworkProvider>();
services.AddExceptionHandler<PluginExceptionHandler>();
services.TryAddSingleton<AppService>();
services.AddTransient<PluginService>();
services.AddSingleton<PluginHookService>();

View File

@ -293,7 +293,6 @@ namespace BTCPayServer.Hosting
app.UseStatusCodePagesWithReExecute("/errors/{0}");
app.UseExceptionHandler("/errors/{0}");
app.UsePayServer();
app.UseRouting();
app.UseCors();

View File

@ -20,6 +20,6 @@ namespace BTCPayServer.Models.InvoicingModels
public Dictionary<string, object> CartData { get; set; }
public ReceiptOptions ReceiptOptions { get; set; }
public List<ViewPaymentRequestViewModel.PaymentRequestInvoicePayment> Payments { get; set; }
public string RedirectUrl { get; set; }
public string OrderUrl { get; set; }
}
}

View File

@ -1,10 +0,0 @@
using Newtonsoft.Json;
namespace BTCPayServer.Services.Altcoins.Monero.RPC.Models
{
public partial class OpenWalletErrorResponse
{
[JsonProperty("code")] public int Code { get; set; }
[JsonProperty("message")] public string Message { get; set; }
}
}

View File

@ -1,10 +0,0 @@
using Newtonsoft.Json;
namespace BTCPayServer.Services.Altcoins.Monero.RPC.Models
{
public partial class OpenWalletRequest
{
[JsonProperty("filename")] public string Filename { get; set; }
[JsonProperty("password")] public string Password { get; set; }
}
}

View File

@ -1,12 +0,0 @@
using Newtonsoft.Json;
namespace BTCPayServer.Services.Altcoins.Monero.RPC.Models
{
public partial class OpenWalletResponse
{
[JsonProperty("id")] public string Id { get; set; }
[JsonProperty("jsonrpc")] public string Jsonrpc { get; set; }
[JsonProperty("result")] public object Result { get; set; }
[JsonProperty("error")] public OpenWalletErrorResponse Error { get; set; }
}
}

View File

@ -1,53 +0,0 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Configuration;
using BTCPayServer.Logging;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Hosting.Internal;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace BTCPayServer.Plugins
{
public class PluginExceptionHandler : IExceptionHandler
{
readonly string _pluginDir;
readonly IHostApplicationLifetime _applicationLifetime;
private readonly Logs _logs;
public PluginExceptionHandler(IOptions<DataDirectories> options, IHostApplicationLifetime applicationLifetime, Logs logs)
{
_applicationLifetime = applicationLifetime;
_logs = logs;
_pluginDir = options.Value.PluginDir;
}
public ValueTask<bool> TryHandleAsync(HttpContext httpContext, Exception exception, CancellationToken cancellationToken)
{
if (!GetDisablePluginIfCrash(httpContext) ||
!PluginManager.IsExceptionByPlugin(exception, out var pluginName))
return ValueTask.FromResult(false);
_logs.Configuration.LogError(exception, $"Unhandled exception caused by plugin '{pluginName}', disabling it and restarting...");
PluginManager.DisablePlugin(_pluginDir, pluginName);
_ = Task.Delay(3000).ContinueWith((t) => _applicationLifetime.StopApplication());
// Returning true here means we will see Error 500 error message.
// Returning false means that the user will see a stacktrace.
return ValueTask.FromResult(false);
}
public static bool GetDisablePluginIfCrash(HttpContext httpContext)
{
return httpContext.Items.TryGetValue("DisablePluginIfCrash", out object renderingDashboard) ||
renderingDashboard is not true;
}
public static void SetDisablePluginIfCrash(HttpContext httpContext)
{
httpContext.Items.TryAdd("DisablePluginIfCrash", true);
}
}
}

View File

@ -258,27 +258,12 @@ namespace BTCPayServer.Plugins
private static IEnumerable<IBTCPayServerPlugin> GetPluginInstancesFromAssembly(Assembly assembly)
{
return GetTypesNotCrash(assembly).Where(type =>
return assembly.GetTypes().Where(type =>
typeof(IBTCPayServerPlugin).IsAssignableFrom(type) && type != typeof(PluginService.AvailablePlugin) &&
!type.IsAbstract).
Select(type => (IBTCPayServerPlugin)Activator.CreateInstance(type, Array.Empty<object>()));
}
private static IEnumerable<Type> GetTypesNotCrash(Assembly assembly)
{
try
{
// Strange crash with selenium
if (assembly.FullName.Contains("Selenium", StringComparison.OrdinalIgnoreCase))
return Array.Empty<Type>();
return assembly.GetTypes();
}
catch(ReflectionTypeLoadException ex)
{
return ex.Types.Where(t => t is not null).ToArray();
}
}
private static IBTCPayServerPlugin GetPluginInstanceFromAssembly(string pluginIdentifier, Assembly assembly)
{
return GetPluginInstancesFromAssembly(assembly).FirstOrDefault(plugin => plugin.Identifier == pluginIdentifier);

View File

@ -356,9 +356,11 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
var receiptData = new JObject();
if (choice is not null)
{
var dict = new Dictionary<string, string> { { "Title", choice.Title } };
if (!string.IsNullOrEmpty(choice.Description)) dict["Description"] = choice.Description;
receiptData = JObject.FromObject(dict);
receiptData = JObject.FromObject(new Dictionary<string, string>
{
{"Title", choice.Title},
{"Description", choice.Description},
});
}
else if (jposData is not null)
{

View File

@ -118,7 +118,7 @@ namespace BTCPayServer.Plugins.Shopify
public async Task<ShopifyOrder> CancelOrder(string orderId)
{
var req = CreateRequest(_credentials.ShopName, HttpMethod.Post,
$"orders/{orderId}/cancel.json?restock=true", null, "2024-04");
$"orders/{orderId}/close.json", null, "2024-04");
var strResp = await SendRequest(req);

View File

@ -70,47 +70,6 @@
},
"applicationUrl": "https://localhost:14142/"
},
"Altcoins": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"BTCPAY_EXPERIMENTALV2_CONFIRM": "true",
"BTCPAY_NETWORK": "regtest",
"BTCPAY_LAUNCHSETTINGS": "true",
"BTCPAY_PORT": "14142",
"BTCPAY_HttpsUseDefaultCertificate": "true",
"BTCPAY_VERBOSE": "true",
"BTCPAY_LTCEXPLORERURL": "http://127.0.0.1:32838/",
"BTCPAY_LBTCEXPLORERURL": "http://127.0.0.1:32838/",
"BTCPAY_BTCLIGHTNING": "type=clightning;server=tcp://127.0.0.1:30993/",
"BTCPAY_BTCEXTERNALLNDREST": "type=lnd-rest;server=http://lnd:lnd@127.0.0.1:35531/;allowinsecure=true",
"BTCPAY_BTCEXTERNALLNDSEEDBACKUP": "../BTCPayServer.Tests/TestData/LndSeedBackup/walletunlock.json",
"BTCPAY_BTCEXTERNALSPARK": "server=/spark/btc/;cookiefile=fake",
"BTCPAY_BTCEXTERNALCHARGE": "server=https://127.0.0.1:53280/mycharge/btc/;cookiefilepath=fake",
"BTCPAY_BTCEXTERNALRTL": "server=/rtl/api/authenticate/cookie;cookiefile=fake",
"BTCPAY_BTCEXTERNALTHUNDERHUB": "server=/thub/sso;cookiefile=fake",
"BTCPAY_BTCEXTERNALTORQ": "server=/torq/cookie-login;cookiefile=fake",
"BTCPAY_EXTERNALSERVICES": "totoservice:totolink;Lightning Terminal:/lit/;",
"BTCPAY_EXTERNALCONFIGURATOR": "passwordfile=testpwd;server=/configurator",
"BTCPAY_BTCEXPLORERURL": "http://127.0.0.1:32838/",
"BTCPAY_ALLOW-ADMIN-REGISTRATION": "true",
"BTCPAY_DISABLE-REGISTRATION": "false",
"ASPNETCORE_ENVIRONMENT": "Development",
"BTCPAY_CHAINS": "btc,ltc,lbtc,xmr",
"BTCPAY_POSTGRES": "User ID=postgres;Include Error Detail=true;Host=127.0.0.1;Port=39372;Database=btcpayserver",
"BTCPAY_DEBUGLOG": "debug.log",
"BTCPAY_TORRCFILE": "../BTCPayServer.Tests/TestData/Tor/torrc",
"BTCPAY_SOCKSENDPOINT": "localhost:9050",
"BTCPAY_DOCKERDEPLOYMENT": "true",
"BTCPAY_RECOMMENDED-PLUGINS": "",
"BTCPAY_CHEATMODE": "true",
"BTCPAY_EXPLORERPOSTGRES": "User ID=postgres;Include Error Detail=true;Host=127.0.0.1;Port=39372;Database=nbxplorer",
"BTCPAY_XMR_DAEMON_URI": "http://127.0.0.1:18081",
"BTCPAY_XMR_WALLET_DAEMON_URI": "http://127.0.0.1:18082",
"BTCPAY_XMR_WALLET_DAEMON_WALLETDIR": "/path/to/monero_wallet"
},
"applicationUrl": "https://localhost:14142/"
},
"Altcoins-HTTPS": {
"commandName": "Project",
"launchBrowser": true,
@ -136,7 +95,7 @@
"BTCPAY_ALLOW-ADMIN-REGISTRATION": "true",
"BTCPAY_DISABLE-REGISTRATION": "false",
"ASPNETCORE_ENVIRONMENT": "Development",
"BTCPAY_CHAINS": "btc,ltc,lbtc,xmr",
"BTCPAY_CHAINS": "btc,ltc,lbtc",
"BTCPAY_POSTGRES": "User ID=postgres;Include Error Detail=true;Host=127.0.0.1;Port=39372;Database=btcpayserver",
"BTCPAY_SSHCONNECTION": "root@127.0.0.1:21622",
"BTCPAY_SSHPASSWORD": "opD3i2282D",
@ -146,10 +105,7 @@
"BTCPAY_DOCKERDEPLOYMENT": "true",
"BTCPAY_RECOMMENDED-PLUGINS": "",
"BTCPAY_CHEATMODE": "true",
"BTCPAY_EXPLORERPOSTGRES": "User ID=postgres;Include Error Detail=true;Host=127.0.0.1;Port=39372;Database=nbxplorer",
"BTCPAY_XMR_DAEMON_URI": "http://127.0.0.1:18081",
"BTCPAY_XMR_WALLET_DAEMON_URI": "http://127.0.0.1:18082",
"BTCPAY_XMR_WALLET_DAEMON_WALLETDIR": "/path/to/monero_wallet"
"BTCPAY_EXPLORERPOSTGRES": "User ID=postgres;Include Error Detail=true;Host=127.0.0.1;Port=39372;Database=nbxplorer"
},
"applicationUrl": "https://localhost:14142/"
}

View File

@ -249,28 +249,10 @@ namespace BTCPayServer.Services.Altcoins.Monero.UI
}
}
try
{
var response = await _MoneroRpcProvider.WalletRpcClients[cryptoCode].SendCommandAsync<OpenWalletRequest, OpenWalletResponse>("open_wallet", new OpenWalletRequest
{
Filename = "wallet",
Password = viewModel.WalletPassword
});
if (response?.Error != null)
{
throw new Exception(response.Error.Message);
}
}
catch (Exception ex)
{
ModelState.AddModelError(nameof(viewModel.AccountIndex), $"Could not open the wallet: {ex.Message}");
return View(viewModel);
}
return RedirectToAction(nameof(GetStoreMoneroLikePaymentMethod), new
{
cryptoCode,
StatusMessage = "View-only wallet files uploaded. The wallet will soon become available."
StatusMessage = "View-only wallet files uploaded. If they are valid the wallet will soon become available."
});
}

View File

@ -29,7 +29,7 @@ public class TransactionLinkProviders : Dictionary<PaymentMethodId, TransactionL
{
foreach ((var pmi, var prov) in this)
{
var overrideLink = links.FirstOrDefault(item =>
var overrideLink = links.SingleOrDefault(item =>
item.CryptoCode.Equals(pmi.CryptoCode, StringComparison.InvariantCultureIgnoreCase) ||
item.CryptoCode.Equals(pmi.ToString(), StringComparison.InvariantCultureIgnoreCase));
prov.OverrideBlockExplorerLink = overrideLink?.Link ?? prov.BlockExplorerLinkDefault;

View File

@ -52,7 +52,6 @@
</button>
</nav>
<section id="payment" v-if="isActive">
<div v-if="srvModel.itemDesc && srvModel.itemDesc !== srvModel.storeName" v-text="srvModel.itemDesc" class="fw-semibold text-center text-muted mb-3"></div>
<div class="d-flex justify-content-center mt-1 text-center">
@if (Model.IsUnsetTopUp)
{

View File

@ -82,7 +82,7 @@
</div>
}
</dl>
<a href="?print=true" class="flex-grow-0 align-self-start btn btn-secondary d-print-none fs-4" target="_blank" id="ReceiptLinkPrint">Print</a>
<a href="?print=true" class="flex-grow-0 align-self-start btn btn-secondary d-print-none fs-4" target="_blank">Print</a>
</div>
}
</div>
@ -173,9 +173,9 @@
</div>
}
}
@if (!string.IsNullOrEmpty(Model.RedirectUrl))
@if (!string.IsNullOrEmpty(Model.OrderUrl))
{
<a href="@Model.RedirectUrl" class="btn btn-secondary rounded-pill mx-auto mt-3" rel="noreferrer noopener" target="_blank">Return to @(string.IsNullOrEmpty(Model.StoreName) ? "store" : Model.StoreName)</a>
<a href="@Model.OrderUrl" class="btn btn-secondary rounded-pill mx-auto mt-3" rel="noreferrer noopener" target="_blank">Return to @(string.IsNullOrEmpty(Model.StoreName) ? "store" : Model.StoreName)</a>
}
</div>
</main>

View File

@ -117,7 +117,7 @@
{
@foreach (var (key, value) in Model.AdditionalData)
{
<tr class="additional-data">
<tr>
<td class="text-secondary">@key</td>
<td class="text-end">@value</td>
</tr>
@ -126,20 +126,20 @@
<td colspan="2"><hr class="w-100 my-0"/></td>
</tr>
}
@if (hasCart && !IsManualEntryCart(Model.CartData))
@if (hasCart && !IsManualEntryCart(Model.AdditionalData))
{
_ = Model.CartData.TryGetValue("cart", out var cart) || Model.CartData.TryGetValue("Cart", out cart);
var hasTotal = Model.CartData.TryGetValue("total", out var total) || Model.CartData.TryGetValue("Total", out total);
var hasSubtotal = Model.CartData.TryGetValue("subtotal", out var subtotal) || Model.CartData.TryGetValue("subTotal", out subtotal) || Model.CartData.TryGetValue("Subtotal", out subtotal);
var hasDiscount = Model.CartData.TryGetValue("discount", out var discount) || Model.CartData.TryGetValue("Discount", out discount);
var hasTip = Model.CartData.TryGetValue("tip", out var tip) || Model.CartData.TryGetValue("Tip", out tip);
if (cart is Dictionary<string, object> { Keys.Count: > 0 } cartDict)
if (cart is Dictionary<string, object> { Keys.Count: > 0 } cartDict)
{
@foreach (var (key, value) in cartDict)
{
<tr class="cart-data">
<td class="key text-secondary">@key</td>
<td class="val text-end">@value</td>
<tr>
<td class="text-secondary">@key</td>
<td class="text-end">@value</td>
</tr>
}
}
@ -148,7 +148,7 @@
@foreach (var value in cartCollection)
{
<tr>
<td class="val text-end">@value</td>
<td class="text-end">@value</td>
</tr>
}
}
@ -157,23 +157,23 @@
<tr>
<td colspan="2"><hr class="w-100 my-0"/></td>
</tr>
<tr class="sums-data">
<td class="key text-secondary">Subtotal</td>
<td class="val text-end">@subtotal</td>
<tr>
<td class="text-secondary">Subtotal</td>
<td class="text-end">@subtotal</td>
</tr>
}
if (hasDiscount)
{
<tr class="sums-data">
<td class="key text-secondary">Discount</td>
<td class="val text-end">@discount</td>
<tr>
<td class="text-secondary">Discount</td>
<td class="text-end">@discount</td>
</tr>
}
if (hasTip)
{
<tr class="sums-data">
<td class="key text-secondary">Tip</td>
<td class="val text-end">@tip</td>
<tr>
<td class="text-secondary">Tip</td>
<td class="text-end">@tip</td>
</tr>
}
if (hasTotal)
@ -181,17 +181,17 @@
<tr>
<td colspan="2"><hr class="w-100 my-0"/></td>
</tr>
<tr class="sums-data">
<td class="key text-secondary">Total</td>
<td class="val text-end fw-semibold">@total</td>
<tr>
<th class="text-secondary">Total</th>
<td class="text-end fw-semibold">@total</td>
</tr>
}
}
else
{
<tr class="sums-data">
<td class="key text-nowrap text-secondary">Total</td>
<td class="val text-end fw-semibold">@DisplayFormatter.Currency(Model.Amount, Model.Currency, DisplayFormatter.CurrencyFormat.Symbol)</td>
<tr>
<td class="text-nowrap text-secondary">Total</td>
<td class="text-end fw-semibold">@DisplayFormatter.Currency(Model.Amount, Model.Currency, DisplayFormatter.CurrencyFormat.Symbol)</td>
</tr>
}
@if (Model.Payments?.Any() is true)
@ -207,25 +207,25 @@
<tr>
<td colspan="2" class="text-nowrap text-secondary">Payment @(i + 1)</td>
</tr>
<tr class="payment-data">
<tr>
<td class="text-nowrap">Received</td>
<td>@payment.ReceivedDate.ToBrowserDate()</td>
</tr>
}
<tr class="payment-data">
<tr>
<td class="text-nowrap text-secondary">@(Model.Payments.Count == 1 ? "Paid" : "")</td>
<td class="text-end">@payment.AmountFormatted</td>
</tr>
<tr class="payment-data">
<tr>
<td colspan="2" class="text-end">@payment.PaidFormatted</td>
</tr>
<tr class="payment-data">
<tr>
<td class="text-nowrap text-secondary">Rate</td>
<td class="text-end">@payment.RateFormatted</td>
</tr>
@if (!string.IsNullOrEmpty(payment.Destination))
{
<tr class="payment-data">
<tr>
<td class="text-nowrap text-secondary">Destination</td>
<td class="text-break">
@if (payment.Destination.Length > 69)
@ -245,7 +245,7 @@
}
@if (!string.IsNullOrEmpty(payment.PaymentProof))
{
<tr class="payment-data">
<tr>
<td class="text-nowrap text-secondary">Pay Proof</td>
<td class="text-break">@payment.PaymentProof</td>
</tr>

View File

@ -3,6 +3,7 @@
@model BTCPayServer.Services.Altcoins.Monero.UI.UIMoneroLikeStoreController.MoneroLikePaymentMethodListViewModel
@{
Layout = "../Shared/_NavLayout.cshtml";
ViewData.SetActivePage("Monero Settings", "Monero Settings", "Monero Settings");
ViewData["NavPartialName"] = "../UIStores/_Nav";
}

View File

@ -20,7 +20,6 @@
#app .table-responsive { max-height: 80vh; }
#app #charts { gap: var(--btcpay-space-l) var(--btcpay-space-xxl); }
#app #charts article { flex: 1 1 450px; }
main .dropdown-menu.show { z-index: 99999; }
</style>
}
@ -134,7 +133,7 @@
<template v-else-if="srv.result.fields[columnIndex].type === 'tx_id'">
<vc:truncate-center text="value" is-vue="true" padding="15" classes="truncate-center-id" link="getExplorerUrl(value, row[columnIndex-1])" />
</template>
<template v-else-if="value && ['Address'].includes(srv.result.fields[columnIndex].name)" >
<template v-else-if="value && ['Address', 'PaymentId'].includes(srv.result.fields[columnIndex].name)" >
<vc:truncate-center text="value" is-vue="true" padding="15" classes="truncate-center-id" />
</template>
<template v-else-if="srv.result.fields[columnIndex].type === 'datetime'">{{ displayDate(value) }}</template>

View File

@ -84,7 +84,7 @@
@plugin
@if (version != null)
{
<span>(@version)</span>
<span>({version})</span>
}
</span>
<form asp-action="UnInstallPlugin" asp-route-plugin="@plugin">

View File

@ -231,7 +231,7 @@
{
var pmi = linkProviders[lpi].Key;
var defaultLink = linkProviders[lpi].Value.BlockExplorerLinkDefault;
var existingOverride = Model.BlockExplorerLinks?.FirstOrDefault(tuple => PaymentMethodId.Parse(tuple.CryptoCode) == pmi);
var existingOverride = Model.BlockExplorerLinks?.SingleOrDefault(tuple => PaymentMethodId.Parse(tuple.CryptoCode) == pmi);
if (existingOverride is null)
{
existingOverride = new PoliciesSettings.BlockExplorerOverrideItem { CryptoCode = pmi.ToStringNormalized(), Link = null };

View File

@ -9,8 +9,7 @@
@using BTCPayServer.Client
@model StoreDashboardViewModel
@{
BTCPayServer.Plugins.PluginExceptionHandler.SetDisablePluginIfCrash(Context);
ViewData.SetActivePage(StoreNavPages.Dashboard, Model.StoreName, Model.StoreId);
ViewData.SetActivePage(StoreNavPages.Dashboard, Model.StoreName, Model.StoreId);
var store = ViewContext.HttpContext.GetStoreData();
}

View File

@ -41,7 +41,6 @@
}
else
{
<input type="hidden" name="UseCustomSMTP" value="true" />
<partial name="EmailsBody" model="Model" />
}

View File

@ -187,7 +187,6 @@
}
function createTable(summaryDefinition, fields, rows) {
rows = clone(rows);
var groupIndices = summaryDefinition.groups.map(g => fields.findIndex((a) => a === g)).filter(g => g !== -1);
var aggregatesIndices = summaryDefinition.aggregates.map(g => fields.findIndex((a) => a === g)).filter(g => g !== -1);
aggregatesIndices = aggregatesIndices.filter(g => g !== -1);

View File

@ -160,11 +160,9 @@ window.BTCPayShopifyIntegrationModule = function () {
}
showPaymentInstructions();
window.onPayButtonClicked = onPayButtonClicked.bind(this);
getOrCheckInvoice(false).then(function (d) {
if (d) {
injectPaymentButtonHtml();
handleInvoiceData(d, { backgroundCheck: true })
}
getOrCheckInvoice(true).then(function (d) {
injectPaymentButtonHtml();
handleInvoiceData(d, {backgroundCheck: true})
});
};

View File

@ -244,11 +244,6 @@
"type": "string",
"nullable": true,
"description": "The maximum amount in sats this ln address allows"
},
"invoiceMetadata": {
"type": "object",
"nullable": true,
"description": "The invoice metadata as JSON."
}
}
}

View File

@ -1,5 +1,5 @@
<Project>
<PropertyGroup>
<Version>1.13.5</Version>
<Version>1.13.2</Version>
</PropertyGroup>
</Project>

View File

@ -1,42 +1,5 @@
# Changelog
## 1.13.5
### Bug fixes
* Fix: Plugin Exception Handler didn't disabled plugin if crash was detected @NicolasDorier
* Fix: Kraken rate provider failing due to bid > ask @NicolasDorier
## 1.13.4
### Bug fixes
* LNUrl payouts failing due to amount restriction wouldn't be immediately cancelled (#6061) @Kukks
* Fix row ordering and display issues in reporting (#6065 #6087, 597e2b0e) @NicolasDorier @dennisreimann
* Parse Timespan strings in the API properly (#6012) @dennisreimann
* "Return to Store" link in invoice receipt should return to the redirectUrl (#6079) @dennisreimann
* Fix crash caused by custom explorer links in some conditions (#6077 #6078) @dennisreimann
* Fix: Can't save email settings on store level (#6076 #6080) @dennisreimann
* Reports: Fix dropdown z-index @dennisreimann
* Shopify: Properly cancel an order when BTCPay invoice expires, and restock the inventory (#6104 #6107 #6108) @NicolasDorier
* Shopify: Generate BTCPay invoice as soon as the payment page in shopify opens (#6105) @NicolasDorier
### Improvements
* Checkout: Display item description if present (#6082) @dennisreimann
* Disable plugins if they crash the Dashboard page (#6099) @NicolasDorier
* Hide empty values in the receipts (#6079) @dennisreimann
* Greenfield: Add the invoice metadata of a Lightning Address (#6067 #6084) @dennisreimann
## 1.13.3
### Bug fixes
* Fix potential crash on receipt print page (#6045) @dennisreimann
* Fix invoice paid for topping up a pull payment didn't top up. @NicolasDorier
* Pull payment: Enable CORS for LNURL request (#6044) @dennisreimann
## 1.13.2
### New features