Compare commits

...

35 Commits

Author SHA1 Message Date
b2480ad081 Update Changelog 2024-07-15 22:00:52 +09:00
c9687622e9 Shopify: Cancel rather than close an order (#6108) 2024-07-15 22:00:39 +09:00
7711acd1e9 Shopify: Create invoice when the payment page opens (#6109) 2024-07-15 22:00:32 +09:00
a580f67991 Add changelog and bump 1.13.4 (#6102)
* Add changelog and bump 1.13.4

* Update Changelog.md

---------

Co-authored-by: d11n <mail@dennisreimann.de>
2024-07-11 22:39:48 +09:00
63a3667406 Timespan in API should be parsed with invariant culture 2024-07-11 21:40:44 +09:00
daeeb58f71 Checkout: Display description if present (#6082) 2024-07-11 21:26:40 +09:00
ff7e96b35f Handle LNURL Payouts failing due to amount restriction (#6061)
* Handle LNURL payouts better when amount is not allowee

* docker-compose: Add missing restart for merchant CLN container

* show sats not msats

---------

Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
2024-07-11 10:37:28 +09:00
43fa3cea53 Truncate Center Component: Fix Vue rendering (#6087)
* Truncate Center Component: Fix Vue rendering

* Simplify the TruncateCenter component

---------

Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
2024-07-11 10:34:22 +09:00
5c2ff32842 Fix Monero development environment with wallet load (#6066) 2024-07-11 10:30:00 +09:00
14113f9468 Add setup script for dev basics like users and stores (#5987)
* Add setup script for dev basics like users and stores

I'm using and extending this script for setting up the basics after I erase my dev containers. Once you clean out everything with `cd BTCPayServer.Tests && docker-compose down --volumes --remove-orphans && docker-compose up dev` you otherwise have to recreate everything manually. This gives you ...

- An admin user with unrestricted API key
- One additional user per default role
- Store 1: Satoshis Steaks with Hot Wallet and Internal Node (and all users assigned to that store)
- Store 2: Nakamoto Nuggets with Hot Wallet and Merchant LND Lightning node (and all users besides Guest assigned to that store)
- Nakamoto Nuggets also gets Cart and Keypad apps
- Store 3 with External Lightning based store with Customer LND Lightning node

## Sample output

```bash
Admin ID: 78aa0b35-6c72-45ac-a7d4-b7976ebbbb62
Admin API Key: 992023ae659295b14c3b429007bbf67c2fec057d

Store Owner ID: e3151462-b0f8-4342-879e-16e42d3432d9
Store Manager ID: d0f11a4d-7c9f-466d-bbb9-dfba09295446
Store Employee ID: 13a882de-65f1-4be9-819e-be058e54a8a9

Satoshis Steaks Store ID: FDyaDcDxtSnNx77nEtT8VL55tcitcrV3Zoj5B6eoByEL

Nakamoto Nuggets Store ID: 8uJqvtPnvCU1XXBiSNBLGEn5XinwC1qYcyP495pPzn9a
Nakamoto Nuggets Keypad POS ID: 2q3Z6b8RUfwrvMyYngNAbj8kPqU8
Nakamoto Nuggets Cart POS ID: 2TfyrzZjiWnYp9QwWyy4U7y5dAFP

External Lightning Store ID: Cr56Ch7h3cgGPcbsZnKWnqCisMojfAdUVJhsN3zLcqSP
```

* Fix path issue
2024-07-11 10:29:47 +09:00
3cc5c07dec Fix tests (#6060)
* docker-compose: Add missing restart for merchant CLN container

* Upgrade ChromeDriver

* Fix path and test
2024-07-11 10:25:39 +09:00
4429d0d631 Disable plugins if they crash the Dashboard page (#6099) 2024-07-11 10:20:02 +09:00
fa7ea62ab2 Add AdditionalData to PointOfSaleBaseData 2024-07-11 10:19:28 +09:00
ef64b11f7a TimeSpan JSON Converter: Parse strings (#6012)
Something I came across while working in the app: Without this, the [TimeSpan default values of the StoreBlob](https://github.com/btcpayserver/btcpayserver/blob/master/BTCPayServer/Data/StoreBlob.cs#L84) cannot be parsed and don't get applied properly.
2024-07-11 10:14:29 +09:00
9cbea55c2a bump selenium container (#6071) 2024-07-11 10:13:06 +09:00
6ad1c962ff Bumping LND to 0.18.1-beta (#6094) 2024-07-11 10:08:48 +09:00
eaef28ae7f Bump EF libs (#6096) 2024-07-11 10:08:33 +09:00
80f9e313bc Fix Monero local dev docker start up (#6042) 2024-07-11 09:59:00 +09:00
f2f97bb468 Greenfield: Fix missing delete app policy (#6098) 2024-07-10 16:11:52 +02:00
69f7eb11bc Receipt page fixes (#6079)
* Receipt: Don't assign empty values to data; hide present empty values

* Receipt: Use same URL on "Return to Store" link as on invoice
2024-07-09 23:56:34 +09:00
5bf5f4fc2b TransactionLinkProviders: Don't force single item (#6078)
* TransactionLinkProviders: Don't force single item

Fixes #6077.

* docker-compose: Add missing restart for merchant CLN container
2024-07-09 22:56:43 +09:00
35041751e0 Fix email settings on store level, when server has no email settings (#6080)
* Fix email settings on store level, when server has no email settings

Fixes #6076.

* docker-compose: Add missing restart for merchant CLN container
2024-07-09 22:51:16 +09:00
8aae388d95 Greenfield: Add missing invoice metadata to Lightning Address (#6084)
* Greenfield: Add missing invoice metadata to Lightning Address

Closes #6067.

* Update BTCPayServer/wwwroot/swagger/v1/swagger.template.stores-lightning-addresses.json
2024-07-04 16:25:25 +02:00
201d6cfe70 Reports: Fix dropdown z-index 2024-07-04 15:37:13 +02:00
597e2b0ec1 Fix: Reports rows weren't always properly sorted (Fix #6065) 2024-07-04 18:09:03 +09:00
058a3ee96a Do not crash when refunding an invoice that has been marked settled (Fix #6003) 2024-07-04 16:50:27 +09:00
05f3539818 Do not crash the plugin packer if Assembly.GetTypes crashes 2024-06-21 10:16:55 +09:00
9d84ec4aa4 bump v 2024-06-13 14:48:48 +02:00
33f20b7be5 v1.13.3: Add Changelog (#6048)
* v1.13.3: Add Changelog

* Update Changelog.md

---------

Co-authored-by: Andrew Camilleri <evilkukka@gmail.com>
2024-06-13 14:47:17 +02:00
487f967607 bump cln 2024-06-12 19:39:55 +02:00
5bc9285e84 Pull payment: Enable CORS for LNURL request (#6044)
Fixes #6043 and ensures Mutiny Wallet can do the LNURL request.
2024-06-12 11:14:58 +02:00
bc1a5aa8f0 Fix null pointer exception on receipt print page (#6045)
* Fix null pointer exception on receipt print page

* Adding test to verify receipt printing working as expected

* Update Chrome WebDriver

* Add print receipt tests

* Remove duplicate test

---------

Co-authored-by: rockstardev <5191402+rockstardev@users.noreply.github.com>
2024-06-11 16:04:25 +02:00
556a9c0e6d Fixing Fast Tests (#6047)
* Fixing Fast Tests

* Revert file header change

---------

Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
2024-06-11 12:53:50 +02:00
a026d244fe Fix: Invoice paid for topping up a pull payment weren't topping anything 2024-06-06 23:11:01 +09:00
5884850e22 POS: Don't show free items in print view (#6009)
We cannot generate a proper LNURL response for free items, hence we should not show them in the print view.

Closes #5999.
2024-06-05 22:12:12 +09:00
57 changed files with 667 additions and 119 deletions

View File

@ -32,9 +32,9 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="HtmlSanitizer" Version="8.0.838" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.5" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4" />
<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/{pullPaymentId}/lnurl",
$"api/v1/pull-payments/{HttpUtility.UrlEncode(pullPaymentId)}/lnurl",
method: HttpMethod.Get), cancellationToken);
return await HandleResponse<PullPaymentLNURL>(response);
}

View File

@ -1,4 +1,5 @@
using System;
using System.Globalization;
using NBitcoin.JsonConverters;
using Newtonsoft.Json;
@ -58,6 +59,8 @@ 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,3 +1,5 @@
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Client.Models;
public class LightningAddressData
@ -6,5 +8,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,5 +1,8 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Client.Models
{
@ -13,7 +16,9 @@ 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.5">
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.6">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.5" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.7" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BTCPayServer.Abstractions\BTCPayServer.Abstractions.csproj" />

View File

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
@ -109,9 +110,22 @@ namespace BTCPayServer.PluginPacker
private static Type[] GetAllExtensionTypesFromAssembly(Assembly assembly)
{
return assembly.GetTypes().Where(type =>
return GetLoadableTypes(assembly).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.9.2" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.10.0" />
<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

@ -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.1.1" />
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="121.0.6167.8500" />
<PackageReference Include="Selenium.WebDriver" Version="4.22.0" />
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="125.0.6422.14100" />
<PackageReference Include="xunit" Version="2.6.6" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.6">
<PrivateAssets>all</PrivateAssets>

View File

@ -47,7 +47,6 @@ 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,6 +2160,17 @@ 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)]
@ -3457,6 +3468,7 @@ 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));
@ -3484,6 +3496,17 @@ 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,6 +2638,33 @@ 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();
@ -2675,7 +2702,6 @@ 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"));
@ -2690,6 +2716,27 @@ 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

@ -272,7 +272,8 @@ namespace BTCPayServer.Tests
"https://www.btse.com", // not allowing to be hit from circleci
"https://www.bitpay.com", // not allowing to be hit from circleci
"https://support.bitpay.com",
"https://www.coingecko.com" // unhappy service
"https://www.coingecko.com", // unhappy service
"https://www.wasabiwallet.io/" // returning Forbidden
};
foreach (var match in regex.Matches(text).OfType<Match>())

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,7 +18,6 @@ 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:101.0
image: selenium/standalone-chrome:125.0
extra_hosts:
- "tests:172.23.0.18"
expose:
@ -163,7 +163,7 @@ services:
- "bitcoin_datadir:/data"
customer_lightningd:
image: btcpayserver/lightning:v24.02.2
image: btcpayserver/lightning:v24.05
stop_signal: SIGKILL
restart: unless-stopped
environment:
@ -191,8 +191,9 @@ services:
- bitcoind
merchant_lightningd:
image: btcpayserver/lightning:v24.02.2
image: btcpayserver/lightning:v24.05
stop_signal: SIGKILL
restart: unless-stopped
environment:
EXPOSE_TCP: "true"
LIGHTNINGD_CHAIN: "btc"
@ -216,6 +217,7 @@ services:
- "merchant_lightningd_datadir:/root/.lightning"
depends_on:
- bitcoind
postgres:
image: postgres:13.4
environment:
@ -226,7 +228,7 @@ services:
- "5432"
merchant_lnd:
image: btcpayserver/lnd:v0.18.0-beta
image: btcpayserver/lnd:v0.18.1-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"
@ -261,7 +263,7 @@ services:
- bitcoind
customer_lnd:
image: btcpayserver/lnd:v0.18.0-beta
image: btcpayserver/lnd:v0.18.1-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"
@ -308,27 +310,29 @@ services:
- "tor_datadir:/home/tor/.tor"
- "torrcdir:/usr/local/etc/tor"
- "tor_servicesdir:/var/lib/tor/hidden_services"
monerod:
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"
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"
monero_wallet:
image: btcpayserver/monero:0.18.2.2-5
image: btcpayserver/monero:0.18.3.3
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-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"
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"
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:101.0
image: selenium/standalone-chrome:125.0
extra_hosts:
- "tests:172.23.0.18"
expose:
@ -149,7 +149,7 @@ services:
- "bitcoin_datadir:/data"
customer_lightningd:
image: btcpayserver/lightning:v24.02.2
image: btcpayserver/lightning:v24.05
stop_signal: SIGKILL
restart: unless-stopped
environment:
@ -177,8 +177,9 @@ services:
- bitcoind
merchant_lightningd:
image: btcpayserver/lightning:v24.02.2
image: btcpayserver/lightning:v24.05
stop_signal: SIGKILL
restart: unless-stopped
environment:
EXPOSE_TCP: "true"
LIGHTNINGD_CHAIN: "btc"
@ -213,7 +214,7 @@ services:
- "5432"
merchant_lnd:
image: btcpayserver/lnd:v0.18.0-beta
image: btcpayserver/lnd:v0.18.1-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"
@ -250,7 +251,7 @@ services:
- bitcoind
customer_lnd:
image: btcpayserver/lnd:v0.18.0-beta
image: btcpayserver/lnd:v0.18.1-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"

View File

@ -0,0 +1,199 @@
#!/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.5" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.5" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="8.0.6" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.7" />
</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"@(!string.IsNullOrEmpty(Model.Id) ? $"id={Model.Id}" : null) data-text=@Safe.Json(Model.Text)>
<span class="truncate-center @classes" id="@Model.Id" data-text="@Model.Text">
@if (Model.IsVue)
{
<span class="truncate-center-truncated" data-bs-toggle="tooltip" :title=@Safe.Json(Model.Text)>
<span class="truncate-center-truncated" data-bs-toggle="tooltip" :title="@Model.Text">
@if (Model.Elastic)
{
<span class="truncate-center-start" v-text=@Safe.Json(Model.Text)></span>
<span class="truncate-center-start" v-text="@Model.Text"></span>
}
else
{
<span class="truncate-center-start" v-text=@Safe.Json($"{Model.Text}.slice(0, {Model.Padding})")></span>
<span>…</span>
<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-end" v-text=@Safe.Json($"{Model.Text}.slice(-{Model.Padding})")></span>
<span class="truncate-center-end" v-text="@(Model.Text).slice(-@(Model.Padding))" v-if="@(Model.Text).length > 2 * @(Model.Padding)"></span>
</span>
<span class="truncate-center-text" v-text=@Safe.Json(Model.Text)></span>
<span class="truncate-center-text" v-text="@Model.Text"></span>
}
else
{
@ -35,13 +35,13 @@
}
@if (Model.Copy)
{
<button type="button" class="btn btn-link p-0" @(Model.IsVue ? ":" : string.Empty)data-clipboard=@Safe.Json(Model.Text)>
<button type="button" class="btn btn-link p-0" @(prefix)data-clipboard="@Model.Text">
<vc:icon symbol="copy" />
</button>
}
@if (!string.IsNullOrEmpty(Model.Link))
{
<a @(Model.IsVue ? ":" : "")href="@Model.Link" rel="noreferrer noopener" target="_blank">
<a @(prefix)href="@Model.Link" rel="noreferrer noopener" target="_blank">
<vc:icon symbol="info" />
</a>
}

View File

@ -213,6 +213,7 @@ 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,6 +401,14 @@ 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(
@ -468,7 +476,6 @@ 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,4 +1,5 @@
#nullable enable
using System;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
@ -8,6 +9,8 @@ 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;
@ -31,12 +34,13 @@ 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
CurrencyCode = blob.CurrencyCode,
InvoiceMetadata = blob.InvoiceMetadata
};
}
@ -83,16 +87,17 @@ 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
CurrencyCode = data.CurrencyCode,
InvoiceMetadata = data.InvoiceMetadata
})))
{
return await GetStoreLightningAddress(storeId, username);

View File

@ -213,7 +213,7 @@ namespace BTCPayServer.Controllers
{
InvoiceId = i.Id,
OrderId = i.Metadata?.OrderId,
OrderUrl = i.Metadata?.OrderUrl,
RedirectUrl = i.RedirectURL?.AbsoluteUri ?? i.Metadata?.OrderUrl,
Status = i.Status.ToModernStatus(),
Currency = i.Currency,
Timestamp = i.InvoiceTime,
@ -249,10 +249,12 @@ namespace BTCPayServer.Controllers
receiptData.Remove(key);
}
}
// assign the rest to additional data
// assign the rest to additional data and remove empty values
if (receiptData.Any())
{
vm.AdditionalData = receiptData;
vm.AdditionalData = receiptData
.Where(x => !string.IsNullOrEmpty(x.Value.ToString()))
.ToDictionary(x => x.Key, x => x.Value);
}
}
@ -366,6 +368,11 @@ 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,11 +90,14 @@ 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,10 +195,11 @@ 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,7 +73,11 @@ namespace BTCPayServer.Data.Payouts.LightningLike
{
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(30));
using var t = CancellationTokenSource.CreateLinkedTokenSource(timeout.Token, cancellationToken);
var info = (LNURLPayRequest)(await LNURL.LNURL.FetchInformation(lnurl, CreateClient(lnurl), t.Token));
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.");
lnurlTag = info.Tag;
}
@ -143,9 +147,27 @@ namespace BTCPayServer.Data.Payouts.LightningLike
return Task.CompletedTask;
}
public Task<decimal> GetMinimumPayoutAmount(PaymentMethodId paymentMethodId, IClaimDestination claimDestination)
public async Task<decimal> GetMinimumPayoutAmount(PaymentMethodId paymentMethod, IClaimDestination claimDestination)
{
return Task.FromResult(Money.Satoshis(1).ToDecimal(MoneyUnit.BTC));
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);
}
public Dictionary<PayoutState, List<(string Action, string Text)>> GetPayoutSpecificActions()

View File

@ -244,13 +244,15 @@ 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.MilliSatoshi}msats"
$"The LNURL provided would not generate an invoice of {lm.ToDecimal(LightMoneyUnit.Satoshi)} sats"
});
}

View File

@ -341,11 +341,11 @@ namespace BTCPayServer.HostedServices
{
payoutHandler.StartBackgroundCheck(Subscribe);
}
_eventAggregator.Subscribe<Events.InvoiceEvent>(TopUpInvoiceCore);
return new[] { Loop() };
}
private void TopUpInvoice(InvoiceEvent evt)
private void TopUpInvoiceCore(InvoiceEvent evt)
{
if (evt.EventCode == InvoiceEventCode.Completed || evt.EventCode == InvoiceEventCode.MarkedCompleted)
{

View File

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

View File

@ -293,6 +293,7 @@ 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 OrderUrl { get; set; }
public string RedirectUrl { get; set; }
}
}

View File

@ -0,0 +1,10 @@
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

@ -0,0 +1,10 @@
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

@ -0,0 +1,12 @@
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

@ -0,0 +1,52 @@
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...");
_ = 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);
}
internal static bool GetDisablePluginIfCrash(HttpContext httpContext)
{
return httpContext.Items.TryGetValue("DisablePluginIfCrash", out object renderingDashboard) ||
renderingDashboard is not true;
}
internal static void SetDisablePluginIfCrash(HttpContext httpContext)
{
httpContext.Items.TryAdd("DisablePluginIfCrash", true);
}
}
}

View File

@ -258,12 +258,27 @@ namespace BTCPayServer.Plugins
private static IEnumerable<IBTCPayServerPlugin> GetPluginInstancesFromAssembly(Assembly assembly)
{
return assembly.GetTypes().Where(type =>
return GetTypesNotCrash(assembly).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,11 +356,9 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
var receiptData = new JObject();
if (choice is not null)
{
receiptData = JObject.FromObject(new Dictionary<string, string>
{
{"Title", choice.Title},
{"Description", choice.Description},
});
var dict = new Dictionary<string, string> { { "Title", choice.Title } };
if (!string.IsNullOrEmpty(choice.Description)) dict["Description"] = choice.Description;
receiptData = JObject.FromObject(dict);
}
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}/close.json", null, "2024-04");
$"orders/{orderId}/cancel.json?restock=true", null, "2024-04");
var strResp = await SendRequest(req);

View File

@ -70,6 +70,47 @@
},
"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,
@ -95,7 +136,7 @@
"BTCPAY_ALLOW-ADMIN-REGISTRATION": "true",
"BTCPAY_DISABLE-REGISTRATION": "false",
"ASPNETCORE_ENVIRONMENT": "Development",
"BTCPAY_CHAINS": "btc,ltc,lbtc",
"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_SSHCONNECTION": "root@127.0.0.1:21622",
"BTCPAY_SSHPASSWORD": "opD3i2282D",
@ -105,7 +146,10 @@
"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_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/"
}

View File

@ -249,10 +249,28 @@ 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. If they are valid the wallet will soon become available."
StatusMessage = "View-only wallet files uploaded. 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.SingleOrDefault(item =>
var overrideLink = links.FirstOrDefault(item =>
item.CryptoCode.Equals(pmi.CryptoCode, StringComparison.InvariantCultureIgnoreCase) ||
item.CryptoCode.Equals(pmi.ToString(), StringComparison.InvariantCultureIgnoreCase));
prov.OverrideBlockExplorerLink = overrideLink?.Link ?? prov.BlockExplorerLinkDefault;

View File

@ -72,6 +72,7 @@ else
{
var item = Model.Items[x];
var formatted = DisplayFormatter.Currency(item.Price ?? 0, Model.CurrencyCode, DisplayFormatter.CurrencyFormat.Symbol);
if (item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Fixed && item.Price == 0) continue;
<div class="d-flex flex-wrap">
<div class="card px-0" data-id="@x">
<div class="card-body p-3 d-flex flex-column gap-2">

View File

@ -52,6 +52,7 @@
</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">Print</a>
<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>
</div>
}
</div>
@ -173,9 +173,9 @@
</div>
}
}
@if (!string.IsNullOrEmpty(Model.OrderUrl))
@if (!string.IsNullOrEmpty(Model.RedirectUrl))
{
<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>
<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>
}
</div>
</main>

View File

@ -117,7 +117,7 @@
{
@foreach (var (key, value) in Model.AdditionalData)
{
<tr>
<tr class="additional-data">
<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.AdditionalData))
@if (hasCart && !IsManualEntryCart(Model.CartData))
{
_ = 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>
<td class="text-secondary">@key</td>
<td class="text-end">@value</td>
<tr class="cart-data">
<td class="key text-secondary">@key</td>
<td class="val text-end">@value</td>
</tr>
}
}
@ -148,7 +148,7 @@
@foreach (var value in cartCollection)
{
<tr>
<td class="text-end">@value</td>
<td class="val text-end">@value</td>
</tr>
}
}
@ -157,23 +157,23 @@
<tr>
<td colspan="2"><hr class="w-100 my-0"/></td>
</tr>
<tr>
<td class="text-secondary">Subtotal</td>
<td class="text-end">@subtotal</td>
<tr class="sums-data">
<td class="key text-secondary">Subtotal</td>
<td class="val text-end">@subtotal</td>
</tr>
}
if (hasDiscount)
{
<tr>
<td class="text-secondary">Discount</td>
<td class="text-end">@discount</td>
<tr class="sums-data">
<td class="key text-secondary">Discount</td>
<td class="val text-end">@discount</td>
</tr>
}
if (hasTip)
{
<tr>
<td class="text-secondary">Tip</td>
<td class="text-end">@tip</td>
<tr class="sums-data">
<td class="key text-secondary">Tip</td>
<td class="val text-end">@tip</td>
</tr>
}
if (hasTotal)
@ -181,17 +181,17 @@
<tr>
<td colspan="2"><hr class="w-100 my-0"/></td>
</tr>
<tr>
<th class="text-secondary">Total</th>
<td class="text-end fw-semibold">@total</td>
<tr class="sums-data">
<td class="key text-secondary">Total</td>
<td class="val text-end fw-semibold">@total</td>
</tr>
}
}
else
{
<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 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>
}
@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>
<tr class="payment-data">
<td class="text-nowrap">Received</td>
<td>@payment.ReceivedDate.ToBrowserDate()</td>
</tr>
}
<tr>
<tr class="payment-data">
<td class="text-nowrap text-secondary">@(Model.Payments.Count == 1 ? "Paid" : "")</td>
<td class="text-end">@payment.AmountFormatted</td>
</tr>
<tr>
<tr class="payment-data">
<td colspan="2" class="text-end">@payment.PaidFormatted</td>
</tr>
<tr>
<tr class="payment-data">
<td class="text-nowrap text-secondary">Rate</td>
<td class="text-end">@payment.RateFormatted</td>
</tr>
@if (!string.IsNullOrEmpty(payment.Destination))
{
<tr>
<tr class="payment-data">
<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>
<tr class="payment-data">
<td class="text-nowrap text-secondary">Pay Proof</td>
<td class="text-break">@payment.PaymentProof</td>
</tr>

View File

@ -3,7 +3,6 @@
@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,6 +20,7 @@
#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>
}
@ -133,7 +134,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', 'PaymentId'].includes(srv.result.fields[columnIndex].name)" >
<template v-else-if="value && ['Address'].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?.SingleOrDefault(tuple => PaymentMethodId.Parse(tuple.CryptoCode) == pmi);
var existingOverride = Model.BlockExplorerLinks?.FirstOrDefault(tuple => PaymentMethodId.Parse(tuple.CryptoCode) == pmi);
if (existingOverride is null)
{
existingOverride = new PoliciesSettings.BlockExplorerOverrideItem { CryptoCode = pmi.ToStringNormalized(), Link = null };

View File

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

View File

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

View File

@ -187,6 +187,7 @@
}
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,9 +160,11 @@ window.BTCPayShopifyIntegrationModule = function () {
}
showPaymentInstructions();
window.onPayButtonClicked = onPayButtonClicked.bind(this);
getOrCheckInvoice(true).then(function (d) {
injectPaymentButtonHtml();
handleInvoiceData(d, {backgroundCheck: true})
getOrCheckInvoice(false).then(function (d) {
if (d) {
injectPaymentButtonHtml();
handleInvoiceData(d, { backgroundCheck: true })
}
});
};

View File

@ -244,6 +244,11 @@
"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.2</Version>
<Version>1.13.4</Version>
</PropertyGroup>
</Project>

View File

@ -1,5 +1,35 @@
# Changelog
## 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