Compare commits
76 Commits
v2.0.0-alp
...
v1.13.4
Author | SHA1 | Date | |
---|---|---|---|
b2480ad081 | |||
c9687622e9 | |||
7711acd1e9 | |||
a580f67991 | |||
63a3667406 | |||
daeeb58f71 | |||
ff7e96b35f | |||
43fa3cea53 | |||
5c2ff32842 | |||
14113f9468 | |||
3cc5c07dec | |||
4429d0d631 | |||
fa7ea62ab2 | |||
ef64b11f7a | |||
9cbea55c2a | |||
6ad1c962ff | |||
eaef28ae7f | |||
80f9e313bc | |||
f2f97bb468 | |||
69f7eb11bc | |||
5bf5f4fc2b | |||
35041751e0 | |||
8aae388d95 | |||
201d6cfe70 | |||
597e2b0ec1 | |||
058a3ee96a | |||
05f3539818 | |||
9d84ec4aa4 | |||
33f20b7be5 | |||
487f967607 | |||
5bc9285e84 | |||
bc1a5aa8f0 | |||
556a9c0e6d | |||
a026d244fe | |||
5884850e22 | |||
b341536e42 | |||
8356c0d5e5 | |||
6d2f886717 | |||
f9aae4ab3d | |||
1ecf0d25a9 | |||
c7231fe092 | |||
8922c3de59 | |||
fefb99dfa2 | |||
a3b0bbe861 | |||
3dd562ffdc | |||
b19db7291d | |||
70253cbd9f | |||
887803a328 | |||
42da90f7dc | |||
1152f68aed | |||
9124aeb1ee | |||
a35c5d8289 | |||
e24b42ef95 | |||
e10937c253 | |||
96b90d2444 | |||
600bbb9ce0 | |||
fe9e5eb9c9 | |||
cb136cba82 | |||
b3240f28b5 | |||
fe32cbd8be | |||
51fcf52da1 | |||
3f02c0d30a | |||
bae1f4e20b | |||
3fbc717cd4 | |||
958a348fed | |||
57226fc97f | |||
ca55e1f300 | |||
8b02c0bd82 | |||
b92ff7c27b | |||
d24761a498 | |||
c78ee24d0a | |||
172dd507bd | |||
fdd4790023 | |||
4ebe46830b | |||
a2df9ed44c | |||
6ae474d214 |
@ -32,9 +32,9 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="HtmlSanitizer" Version="8.0.838" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.1" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.0" />
|
||||
<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.4" />
|
||||
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.0-beta.2" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
@ -5,6 +5,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Npgsql;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Migrations.Operations;
|
||||
|
||||
@ -85,10 +86,9 @@ namespace BTCPayServer.Abstractions.Contracts
|
||||
{
|
||||
o.EnableRetryOnFailure(10);
|
||||
o.SetPostgresVersion(12, 0);
|
||||
if (!string.IsNullOrEmpty(_schemaPrefix))
|
||||
{
|
||||
o.MigrationsHistoryTable(_schemaPrefix);
|
||||
}
|
||||
var mainSearchPath = GetSearchPath(_options.Value.ConnectionString);
|
||||
var schemaPrefix = string.IsNullOrEmpty(_schemaPrefix) ? "__EFMigrationsHistory" : _schemaPrefix;
|
||||
o.MigrationsHistoryTable(schemaPrefix, mainSearchPath);
|
||||
})
|
||||
.ReplaceService<IMigrationsSqlGenerator, CustomNpgsqlMigrationsSqlGenerator>();
|
||||
break;
|
||||
@ -108,5 +108,11 @@ namespace BTCPayServer.Abstractions.Contracts
|
||||
}
|
||||
}
|
||||
|
||||
private string GetSearchPath(string connectionString)
|
||||
{
|
||||
var connectionStringBuilder = new NpgsqlConnectionStringBuilder(connectionString);
|
||||
var searchPaths = connectionStringBuilder.SearchPath?.Split(',');
|
||||
return searchPaths is not { Length: > 0 } ? null : searchPaths[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -36,6 +36,17 @@ public static class HttpRequestExtensions
|
||||
request.Path.ToUriComponent());
|
||||
}
|
||||
|
||||
public static string GetCurrentUrlWithQueryString(this HttpRequest request)
|
||||
{
|
||||
return string.Concat(
|
||||
request.Scheme,
|
||||
"://",
|
||||
request.Host.ToUriComponent(),
|
||||
request.PathBase.ToUriComponent(),
|
||||
request.Path.ToUriComponent(),
|
||||
request.QueryString.ToUriComponent());
|
||||
}
|
||||
|
||||
public static string GetCurrentPath(this HttpRequest request)
|
||||
{
|
||||
return string.Concat(
|
||||
|
@ -31,7 +31,7 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BTCPayServer.Lightning.Common" Version="1.5.1" />
|
||||
<PackageReference Include="NBitcoin" Version="7.0.34" />
|
||||
<PackageReference Include="NBitcoin" Version="7.0.37" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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; }
|
||||
}
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="4.3.0" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="4.3.1" />
|
||||
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="2.0.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(Altcoins)' != 'true'">
|
||||
|
@ -3,11 +3,11 @@
|
||||
<Import Project="../Build/Common.csproj" />
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.1">
|
||||
<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.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.7" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\BTCPayServer.Abstractions\BTCPayServer.Abstractions.csproj" />
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,9 +4,9 @@
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" />
|
||||
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.9" />
|
||||
<PackageReference Include="NBitcoin" Version="7.0.34" />
|
||||
<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" />
|
||||
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="1.0.4" />
|
||||
</ItemGroup>
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
@ -1347,17 +1346,22 @@ bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku
|
||||
|
||||
filter = "status:abed, status:abed2";
|
||||
search = new SearchString(filter);
|
||||
Assert.Equal("", search.TextSearch);
|
||||
Assert.Null(search.TextSearch);
|
||||
Assert.Null(search.TextFilters);
|
||||
Assert.Equal("status:abed, status:abed2", search.ToString());
|
||||
Assert.Throws<KeyNotFoundException>(() => search.Filters["test"]);
|
||||
Assert.Equal(2, search.Filters["status"].Count);
|
||||
Assert.Equal("abed", search.Filters["status"].First());
|
||||
Assert.Equal("abed2", search.Filters["status"].Skip(1).First());
|
||||
|
||||
filter = "StartDate:2019-04-25 01:00 AM, hekki";
|
||||
filter = "StartDate:2019-04-25 01:00 AM, hekki,orderid:MYORDERID,orderid:MYORDERID_2";
|
||||
search = new SearchString(filter);
|
||||
Assert.Equal("2019-04-25 01:00 AM", search.Filters["startdate"].First());
|
||||
Assert.Equal("hekki", search.TextSearch);
|
||||
Assert.Equal("orderid:MYORDERID,orderid:MYORDERID_2", search.TextFilters);
|
||||
Assert.Equal("orderid:MYORDERID,orderid:MYORDERID_2,hekki", search.TextCombined);
|
||||
Assert.Equal("StartDate:2019-04-25 01:00 AM", search.WithoutSearchText());
|
||||
Assert.Equal(filter, search.ToString());
|
||||
|
||||
// modify search
|
||||
filter = $"status:settled,exceptionstatus:paidLate,unusual:true, fulltext searchterm, storeid:{storeId},startdate:2019-04-25 01:00:00";
|
||||
|
@ -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)]
|
||||
|
@ -2620,9 +2620,9 @@ namespace BTCPayServer.Tests
|
||||
|
||||
// Receipt
|
||||
s.Driver.WaitForElement(By.Id("ReceiptLink")).Click();
|
||||
var additionalData = s.Driver.FindElement(By.CssSelector("#AdditionalData table"));
|
||||
var items = additionalData.FindElements(By.CssSelector("tbody tr"));
|
||||
var sums = additionalData.FindElements(By.CssSelector("tfoot tr"));
|
||||
var cartData = s.Driver.FindElement(By.CssSelector("#CartData table"));
|
||||
var items = cartData.FindElements(By.CssSelector("tbody tr"));
|
||||
var sums = cartData.FindElements(By.CssSelector("tfoot tr"));
|
||||
Assert.Equal(2, items.Count);
|
||||
Assert.Equal(4, sums.Count);
|
||||
Assert.Contains("Manual entry 1", items[0].FindElement(By.CssSelector("th")).Text);
|
||||
@ -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,22 +2702,40 @@ namespace BTCPayServer.Tests
|
||||
|
||||
// Receipt
|
||||
s.Driver.WaitForElement(By.Id("ReceiptLink")).Click();
|
||||
|
||||
additionalData = s.Driver.FindElement(By.CssSelector("#AdditionalData table"));
|
||||
items = additionalData.FindElements(By.CssSelector("tbody tr"));
|
||||
sums = additionalData.FindElements(By.CssSelector("tfoot tr"));
|
||||
cartData = s.Driver.FindElement(By.CssSelector("#CartData table"));
|
||||
items = cartData.FindElements(By.CssSelector("tbody tr"));
|
||||
sums = cartData.FindElements(By.CssSelector("tfoot tr"));
|
||||
Assert.Equal(3, items.Count);
|
||||
Assert.Equal(2, sums.Count);
|
||||
Assert.Single(sums);
|
||||
Assert.Contains("Black Tea", items[0].FindElement(By.CssSelector("th")).Text);
|
||||
Assert.Contains("1 x 1,00 € = 1,00 €", items[0].FindElement(By.CssSelector("td")).Text);
|
||||
Assert.Contains("Green Tea", items[1].FindElement(By.CssSelector("th")).Text);
|
||||
Assert.Contains("2 x 1,00 € = 2,00 €", items[1].FindElement(By.CssSelector("td")).Text);
|
||||
Assert.Contains("Manual entry 1", items[2].FindElement(By.CssSelector("th")).Text);
|
||||
Assert.Contains("1,23 €", items[2].FindElement(By.CssSelector("td")).Text);
|
||||
Assert.Contains("Subtotal", sums[0].FindElement(By.CssSelector("th")).Text);
|
||||
Assert.Contains("Total", sums[0].FindElement(By.CssSelector("th")).Text);
|
||||
Assert.Contains("4,23 €", sums[0].FindElement(By.CssSelector("td")).Text);
|
||||
Assert.Contains("Total", sums[1].FindElement(By.CssSelector("th")).Text);
|
||||
Assert.Contains("4,23 €", sums[1].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();
|
||||
@ -2837,9 +2882,9 @@ namespace BTCPayServer.Tests
|
||||
|
||||
// Receipt
|
||||
s.Driver.WaitForElement(By.Id("ReceiptLink")).Click();
|
||||
var additionalData = s.Driver.FindElement(By.CssSelector("#AdditionalData table"));
|
||||
var items = additionalData.FindElements(By.CssSelector("tbody tr"));
|
||||
var sums = additionalData.FindElements(By.CssSelector("tfoot tr"));
|
||||
var cartData = s.Driver.FindElement(By.CssSelector("#CartData table"));
|
||||
var items = cartData.FindElements(By.CssSelector("tbody tr"));
|
||||
var sums = cartData.FindElements(By.CssSelector("tfoot tr"));
|
||||
Assert.Equal(7, items.Count);
|
||||
Assert.Equal(4, sums.Count);
|
||||
Assert.Contains("Black Tea", items[0].FindElement(By.CssSelector("th")).Text);
|
||||
|
@ -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>())
|
||||
@ -464,7 +465,7 @@ retry:
|
||||
|
||||
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "dom-confetti", "dom-confetti.min.js").Trim();
|
||||
version = Regex.Match(actual, "Original file: /npm/dom-confetti@([0-9]+.[0-9]+.[0-9]+)/lib/main.js").Groups[1].Value;
|
||||
expected = (await (await client.GetAsync($"https://cdn.jsdelivr.net/npm/dom-confetti@{version}/lib/main.min.js")).Content.ReadAsStringAsync()).Trim();
|
||||
expected = (await (await client.GetAsync($"https://cdn.jsdelivr.net/npm/dom-confetti@{version}")).Content.ReadAsStringAsync()).Trim();
|
||||
EqualJsContent(expected, actual);
|
||||
|
||||
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "vue-sortable", "sortable.min.js").Trim();
|
||||
|
@ -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);
|
||||
}
|
||||
@ -2985,7 +2985,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Selenium", "Selenium")]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanCreateReports()
|
||||
{
|
||||
using var tester = CreateServerTester(newDb: true);
|
||||
@ -3093,6 +3093,48 @@ namespace BTCPayServer.Tests
|
||||
var invoiceIdIndex = report.GetIndex("InvoiceId");
|
||||
var oldPaymentsCount = report.Data.Count(d => d[invoiceIdIndex].Value<string>() == "Q7RqoHLngK9svM4MgRyi9y");
|
||||
Assert.Equal(8, oldPaymentsCount); // 10 payments, but 2 unaccounted
|
||||
|
||||
var addr = await tester.ExplorerNode.GetNewAddressAsync();
|
||||
// Two invoices get refunded
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
var inv = await client.CreateInvoice(acc.StoreId, new CreateInvoiceRequest() { Amount = 10m, Currency = "USD" });
|
||||
await acc.PayInvoice(inv.Id);
|
||||
await client.MarkInvoiceStatus(acc.StoreId, inv.Id, new MarkInvoiceStatusRequest() { Status = InvoiceStatus.Settled });
|
||||
var refund = await client.RefundInvoice(acc.StoreId, inv.Id, new RefundInvoiceRequest() { RefundVariant = RefundVariant.Fiat, PaymentMethod = "BTC" });
|
||||
|
||||
async Task AssertData(string currency, decimal awaiting, decimal limit, decimal completed, bool fullyPaid)
|
||||
{
|
||||
report = await GetReport(acc, new() { ViewName = "Refunds" });
|
||||
var currencyIndex = report.GetIndex("Currency");
|
||||
var awaitingIndex = report.GetIndex("Awaiting");
|
||||
var fullyPaidIndex = report.GetIndex("FullyPaid");
|
||||
var completedIndex = report.GetIndex("Completed");
|
||||
var limitIndex = report.GetIndex("Limit");
|
||||
var d = Assert.Single(report.Data.Where(d => d[report.GetIndex("InvoiceId")].Value<string>() == inv.Id));
|
||||
Assert.Equal(fullyPaid, (bool)d[fullyPaidIndex]);
|
||||
Assert.Equal(currency, d[currencyIndex].Value<string>());
|
||||
Assert.Equal(completed, (((JObject)d[completedIndex])["v"]).Value<decimal>());
|
||||
Assert.Equal(awaiting, (((JObject)d[awaitingIndex])["v"]).Value<decimal>());
|
||||
Assert.Equal(limit, (((JObject)d[limitIndex])["v"]).Value<decimal>());
|
||||
}
|
||||
|
||||
await AssertData("USD", awaiting: 0.0m, limit: 10.0m, completed: 0.0m, fullyPaid: false);
|
||||
var payout = await client.CreatePayout(refund.Id, new CreatePayoutRequest() { Destination = addr.ToString(), PaymentMethod = "BTC" });
|
||||
await AssertData("USD", awaiting: 10.0m, limit: 10.0m, completed: 0.0m, fullyPaid: false);
|
||||
await client.ApprovePayout(acc.StoreId, payout.Id, new ApprovePayoutRequest());
|
||||
await AssertData("USD", awaiting: 10.0m, limit: 10.0m, completed: 0.0m, fullyPaid: false);
|
||||
if (i == 0)
|
||||
{
|
||||
await client.MarkPayoutPaid(acc.StoreId, payout.Id);
|
||||
await AssertData("USD", awaiting: 0.0m, limit: 10.0m, completed: 10.0m, fullyPaid: true);
|
||||
}
|
||||
if (i == 1)
|
||||
{
|
||||
await client.CancelPayout(acc.StoreId, payout.Id);
|
||||
await AssertData("USD", awaiting: 0.0m, limit: 10.0m, completed: 0.0m, fullyPaid: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<StoreReportResponse> GetReport(TestAccount acc, StoreReportRequest req)
|
||||
|
@ -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;
|
||||
|
@ -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:
|
||||
@ -99,7 +99,7 @@ services:
|
||||
custom:
|
||||
|
||||
nbxplorer:
|
||||
image: nicolasdorier/nbxplorer:2.5.0
|
||||
image: nicolasdorier/nbxplorer:2.5.4
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "32838:32838"
|
||||
@ -163,7 +163,7 @@ services:
|
||||
- "bitcoin_datadir:/data"
|
||||
|
||||
customer_lightningd:
|
||||
image: btcpayserver/lightning:v23.08-dev
|
||||
image: btcpayserver/lightning:v24.05
|
||||
stop_signal: SIGKILL
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
@ -171,6 +171,7 @@ services:
|
||||
LIGHTNINGD_CHAIN: "btc"
|
||||
LIGHTNINGD_NETWORK: "regtest"
|
||||
LIGHTNINGD_OPT: |
|
||||
developer
|
||||
bitcoin-datadir=/etc/bitcoin
|
||||
bitcoin-rpcconnect=bitcoind
|
||||
announce-addr=customer_lightningd:9735
|
||||
@ -190,13 +191,15 @@ services:
|
||||
- bitcoind
|
||||
|
||||
merchant_lightningd:
|
||||
image: btcpayserver/lightning:v23.08-dev
|
||||
image: btcpayserver/lightning:v24.05
|
||||
stop_signal: SIGKILL
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
EXPOSE_TCP: "true"
|
||||
LIGHTNINGD_CHAIN: "btc"
|
||||
LIGHTNINGD_NETWORK: "regtest"
|
||||
LIGHTNINGD_OPT: |
|
||||
developer
|
||||
bitcoin-datadir=/etc/bitcoin
|
||||
bitcoin-rpcconnect=bitcoind
|
||||
announce-addr=merchant_lightningd:9735
|
||||
@ -214,6 +217,7 @@ services:
|
||||
- "merchant_lightningd_datadir:/root/.lightning"
|
||||
depends_on:
|
||||
- bitcoind
|
||||
|
||||
postgres:
|
||||
image: postgres:13.4
|
||||
environment:
|
||||
@ -224,7 +228,7 @@ services:
|
||||
- "5432"
|
||||
|
||||
merchant_lnd:
|
||||
image: btcpayserver/lnd:v0.17.4-beta
|
||||
image: btcpayserver/lnd:v0.18.1-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
@ -259,7 +263,7 @@ services:
|
||||
- bitcoind
|
||||
|
||||
customer_lnd:
|
||||
image: btcpayserver/lnd:v0.17.4-beta
|
||||
image: btcpayserver/lnd:v0.18.1-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
@ -306,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
|
||||
|
@ -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:
|
||||
@ -96,7 +96,7 @@ services:
|
||||
custom:
|
||||
|
||||
nbxplorer:
|
||||
image: nicolasdorier/nbxplorer:2.5.0
|
||||
image: nicolasdorier/nbxplorer:2.5.4
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "32838:32838"
|
||||
@ -149,7 +149,7 @@ services:
|
||||
- "bitcoin_datadir:/data"
|
||||
|
||||
customer_lightningd:
|
||||
image: btcpayserver/lightning:v23.08-dev
|
||||
image: btcpayserver/lightning:v24.05
|
||||
stop_signal: SIGKILL
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
@ -157,6 +157,7 @@ services:
|
||||
LIGHTNINGD_CHAIN: "btc"
|
||||
LIGHTNINGD_NETWORK: "regtest"
|
||||
LIGHTNINGD_OPT: |
|
||||
developer
|
||||
bitcoin-datadir=/etc/bitcoin
|
||||
bitcoin-rpcconnect=bitcoind
|
||||
announce-addr=customer_lightningd:9735
|
||||
@ -176,13 +177,15 @@ services:
|
||||
- bitcoind
|
||||
|
||||
merchant_lightningd:
|
||||
image: btcpayserver/lightning:v23.08-dev
|
||||
image: btcpayserver/lightning:v24.05
|
||||
stop_signal: SIGKILL
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
EXPOSE_TCP: "true"
|
||||
LIGHTNINGD_CHAIN: "btc"
|
||||
LIGHTNINGD_NETWORK: "regtest"
|
||||
LIGHTNINGD_OPT: |
|
||||
developer
|
||||
bitcoin-datadir=/etc/bitcoin
|
||||
bitcoin-rpcconnect=bitcoind
|
||||
announce-addr=merchant_lightningd:9735
|
||||
@ -211,7 +214,7 @@ services:
|
||||
- "5432"
|
||||
|
||||
merchant_lnd:
|
||||
image: btcpayserver/lnd:v0.17.4-beta
|
||||
image: btcpayserver/lnd:v0.18.1-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
@ -248,7 +251,7 @@ services:
|
||||
- bitcoind
|
||||
|
||||
customer_lnd:
|
||||
image: btcpayserver/lnd:v0.17.4-beta
|
||||
image: btcpayserver/lnd:v0.18.1-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
|
199
BTCPayServer.Tests/setup-dev-basics.sh
Executable file
199
BTCPayServer.Tests/setup-dev-basics.sh
Executable 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&s instatni polivka\",\"image\":\"https://i.imgur.com/Y8LCJbG.png\",\"priceType\":\"Fixed\",\"price\":\"60\",\"title\":\"M&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
|
@ -46,13 +46,13 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BTCPayServer.NTag424" Version="1.0.22" />
|
||||
<PackageReference Include="BTCPayServer.NTag424" Version="1.0.23" />
|
||||
<PackageReference Include="YamlDotNet" Version="8.0.0" />
|
||||
<PackageReference Include="BIP78.Sender" Version="0.2.2" />
|
||||
<PackageReference Include="BTCPayServer.Hwi" Version="2.0.2" />
|
||||
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.5.4" />
|
||||
<PackageReference Include="CsvHelper" Version="15.0.5" />
|
||||
<PackageReference Include="Dapper" Version="2.1.28" />
|
||||
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.6.0" />
|
||||
<PackageReference Include="CsvHelper" Version="32.0.3" />
|
||||
<PackageReference Include="Dapper" Version="2.1.35" />
|
||||
<PackageReference Include="Fido2" Version="2.0.2" />
|
||||
<PackageReference Include="Fido2.AspNet" Version="2.0.2" />
|
||||
<PackageReference Include="LNURL" Version="0.0.34" />
|
||||
@ -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.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="8.0.6" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.7" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -34,34 +34,36 @@
|
||||
}
|
||||
else if (Model.Invoices.Any())
|
||||
{
|
||||
<table class="table table-hover mt-3 mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-125px">Date</th>
|
||||
<th class="text-nowrap">Invoice Id</th>
|
||||
<th>Status</th>
|
||||
<th class="text-end">Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var invoice in Model.Invoices)
|
||||
{
|
||||
<div class="table-responsive mt-3 mb-0">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>@invoice.Date.ToTimeAgo()</td>
|
||||
<td>
|
||||
<a asp-controller="UIInvoice" asp-action="Invoice" asp-route-invoiceId="@invoice.InvoiceId" class="text-break">@invoice.InvoiceId</a>
|
||||
</td>
|
||||
<td>
|
||||
<vc:invoice-status state="invoice.Status" payments="invoice.Details.Payments" invoice-id="@invoice.InvoiceId"
|
||||
is-archived="invoice.Details.Archived" has-refund="invoice.HasRefund" />
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<span data-sensitive>@DisplayFormatter.Currency(invoice.Amount, invoice.Currency)</span>
|
||||
</td>
|
||||
<th class="w-125px">Date</th>
|
||||
<th class="text-nowrap">Invoice Id</th>
|
||||
<th>Status</th>
|
||||
<th class="text-end">Amount</th>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var invoice in Model.Invoices)
|
||||
{
|
||||
<tr>
|
||||
<td>@invoice.Date.ToTimeAgo()</td>
|
||||
<td>
|
||||
<a asp-controller="UIInvoice" asp-action="Invoice" asp-route-invoiceId="@invoice.InvoiceId" class="text-break">@invoice.InvoiceId</a>
|
||||
</td>
|
||||
<td>
|
||||
<vc:invoice-status state="invoice.Status" payments="invoice.Details.Payments" invoice-id="@invoice.InvoiceId"
|
||||
is-archived="invoice.Details.Archived" has-refund="invoice.HasRefund" />
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<span data-sensitive>@DisplayFormatter.Currency(invoice.Amount, invoice.Currency)</span>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -31,61 +31,63 @@
|
||||
}
|
||||
else if (Model.Transactions.Any())
|
||||
{
|
||||
<table class="table table-hover mt-3 mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-125px">Date</th>
|
||||
<th>Transaction</th>
|
||||
<th>Labels</th>
|
||||
<th class="text-end">Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var tx in Model.Transactions)
|
||||
{
|
||||
<div class="table-responsive mt-3 mb-0">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>@tx.Timestamp.ToTimeAgo()</td>
|
||||
<td>
|
||||
<vc:truncate-center text="@tx.Id" link="@tx.Link" classes="truncate-center-id" />
|
||||
</td>
|
||||
<td>
|
||||
@if (tx.Labels.Any())
|
||||
{
|
||||
<div class="d-flex flex-wrap gap-2 align-items-center">
|
||||
@foreach (var label in tx.Labels)
|
||||
{
|
||||
<div class="transaction-label" style="--label-bg:@label.Color;--label-fg:@label.TextColor">
|
||||
<span>@label.Text</span>
|
||||
@if (!string.IsNullOrEmpty(label.Link))
|
||||
{
|
||||
<a class="transaction-label-info transaction-details-icon" href="@label.Link"
|
||||
target="_blank" rel="noreferrer noopener" title="@label.Tooltip" data-bs-html="true"
|
||||
data-bs-toggle="tooltip" data-bs-custom-class="transaction-label-tooltip">
|
||||
<vc:icon symbol="info" />
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
</td>
|
||||
@if (tx.Positive)
|
||||
{
|
||||
<td class="text-end text-success">
|
||||
<span data-sensitive>@DisplayFormatter.Currency(tx.Balance, tx.Currency)</span>
|
||||
</td>
|
||||
}
|
||||
else
|
||||
{
|
||||
<td class="text-end text-danger">
|
||||
<span data-sensitive>@DisplayFormatter.Currency(tx.Balance, tx.Currency)</span>
|
||||
</td>
|
||||
}
|
||||
<th class="w-125px">Date</th>
|
||||
<th>Transaction</th>
|
||||
<th>Labels</th>
|
||||
<th class="text-end">Amount</th>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var tx in Model.Transactions)
|
||||
{
|
||||
<tr>
|
||||
<td>@tx.Timestamp.ToTimeAgo()</td>
|
||||
<td>
|
||||
<vc:truncate-center text="@tx.Id" link="@tx.Link" classes="truncate-center-id" />
|
||||
</td>
|
||||
<td>
|
||||
@if (tx.Labels.Any())
|
||||
{
|
||||
<div class="d-flex flex-wrap gap-2 align-items-center">
|
||||
@foreach (var label in tx.Labels)
|
||||
{
|
||||
<div class="transaction-label" style="--label-bg:@label.Color;--label-fg:@label.TextColor">
|
||||
<span>@label.Text</span>
|
||||
@if (!string.IsNullOrEmpty(label.Link))
|
||||
{
|
||||
<a class="transaction-label-info transaction-details-icon" href="@label.Link"
|
||||
target="_blank" rel="noreferrer noopener" title="@label.Tooltip" data-bs-html="true"
|
||||
data-bs-toggle="tooltip" data-bs-custom-class="transaction-label-tooltip">
|
||||
<vc:icon symbol="info" />
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
</td>
|
||||
@if (tx.Positive)
|
||||
{
|
||||
<td class="text-end text-success">
|
||||
<span data-sensitive>@DisplayFormatter.Currency(tx.Balance, tx.Currency)</span>
|
||||
</td>
|
||||
}
|
||||
else
|
||||
{
|
||||
<td class="text-end text-danger">
|
||||
<span data-sensitive>@DisplayFormatter.Currency(tx.Balance, tx.Currency)</span>
|
||||
</td>
|
||||
}
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -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>
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -14,7 +14,6 @@ using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.NTag424;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Security;
|
||||
@ -29,7 +28,6 @@ using Microsoft.Extensions.Logging;
|
||||
using NBitcoin.DataEncoders;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Org.BouncyCastle.Bcpg.OpenPgp;
|
||||
using MarkPayoutRequest = BTCPayServer.HostedServices.MarkPayoutRequest;
|
||||
|
||||
namespace BTCPayServer.Controllers.Greenfield
|
||||
@ -49,7 +47,6 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly SettingsRepository _settingsRepository;
|
||||
private readonly BTCPayServerEnvironment _env;
|
||||
private readonly Logs _logs;
|
||||
|
||||
public GreenfieldPullPaymentController(PullPaymentHostedService pullPaymentService,
|
||||
LinkGenerator linkGenerator,
|
||||
@ -60,7 +57,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
BTCPayNetworkProvider btcPayNetworkProvider,
|
||||
IAuthorizationService authorizationService,
|
||||
SettingsRepository settingsRepository,
|
||||
BTCPayServerEnvironment env, Logs logs)
|
||||
BTCPayServerEnvironment env)
|
||||
{
|
||||
_pullPaymentService = pullPaymentService;
|
||||
_linkGenerator = linkGenerator;
|
||||
@ -72,7 +69,6 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
_authorizationService = authorizationService;
|
||||
_settingsRepository = settingsRepository;
|
||||
_env = env;
|
||||
_logs = logs;
|
||||
}
|
||||
|
||||
[HttpGet("~/api/v1/stores/{storeId}/pull-payments")]
|
||||
@ -161,20 +157,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
}
|
||||
if (!ModelState.IsValid)
|
||||
return this.CreateValidationError(ModelState);
|
||||
var ppId = await _pullPaymentService.CreatePullPayment(new CreatePullPayment()
|
||||
{
|
||||
StartsAt = request.StartsAt,
|
||||
ExpiresAt = request.ExpiresAt,
|
||||
Period = request.Period,
|
||||
BOLT11Expiration = request.BOLT11Expiration,
|
||||
Name = request.Name,
|
||||
Description = request.Description,
|
||||
Amount = request.Amount,
|
||||
Currency = request.Currency,
|
||||
StoreId = storeId,
|
||||
PaymentMethodIds = paymentMethods,
|
||||
AutoApproveClaims = request.AutoApproveClaims
|
||||
});
|
||||
var ppId = await _pullPaymentService.CreatePullPayment(storeId, request);
|
||||
var pp = await _pullPaymentService.GetPullPayment(ppId, false);
|
||||
return this.Ok(CreatePullPaymentData(pp));
|
||||
}
|
||||
@ -212,8 +195,6 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
if (pullPaymentId is null)
|
||||
return PullPaymentNotFound();
|
||||
this._logs.PayServer.LogInformation($"RegisterBoltcard: onExisting queryParam: {onExisting}");
|
||||
this._logs.PayServer.LogInformation($"{JsonConvert.SerializeObject(request)}");
|
||||
var pp = await _pullPaymentService.GetPullPayment(pullPaymentId, false);
|
||||
if (pp is null)
|
||||
return PullPaymentNotFound();
|
||||
@ -259,13 +240,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
_ => request.OnExisting
|
||||
};
|
||||
|
||||
this._logs.PayServer.LogInformation($"After");
|
||||
this._logs.PayServer.LogInformation($"{JsonConvert.SerializeObject(request)}");
|
||||
|
||||
var version = await _dbContextFactory.LinkBoltcardToPullPayment(pullPaymentId, issuerKey, request.UID, request.OnExisting);
|
||||
this._logs.PayServer.LogInformation($"Version: " + version);
|
||||
this._logs.PayServer.LogInformation($"ID: " + Encoders.Hex.EncodeData(issuerKey.GetId(request.UID)));
|
||||
|
||||
var keys = issuerKey.CreatePullPaymentCardKey(request.UID, version, pullPaymentId).DeriveBoltcardKeys(issuerKey);
|
||||
|
||||
var boltcardUrl = Url.Action(nameof(UIBoltcardController.GetWithdrawRequest), "UIBoltcard");
|
||||
@ -282,9 +257,6 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
K3 = Encoders.Hex.EncodeData(keys.K3.ToBytes()).ToUpperInvariant(),
|
||||
K4 = Encoders.Hex.EncodeData(keys.K4.ToBytes()).ToUpperInvariant(),
|
||||
};
|
||||
this._logs.PayServer.LogInformation($"Response");
|
||||
this._logs.PayServer.LogInformation($"{JsonConvert.SerializeObject(resp)}");
|
||||
|
||||
return Ok(resp);
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -162,10 +162,10 @@ namespace BTCPayServer.Controllers
|
||||
model.Overpaid = details.Overpaid;
|
||||
model.StillDue = details.StillDue;
|
||||
model.HasRates = details.HasRates;
|
||||
|
||||
if (additionalData.ContainsKey("receiptData"))
|
||||
|
||||
if (additionalData.TryGetValue("receiptData", out object? receiptData))
|
||||
{
|
||||
model.ReceiptData = (Dictionary<string, object>)additionalData["receiptData"];
|
||||
model.ReceiptData = (Dictionary<string, object>)receiptData;
|
||||
additionalData.Remove("receiptData");
|
||||
}
|
||||
|
||||
@ -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,
|
||||
@ -226,15 +226,42 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
JToken? receiptData = null;
|
||||
i.Metadata?.AdditionalData?.TryGetValue("receiptData", out receiptData);
|
||||
|
||||
var metaData = PosDataParser.ParsePosData(i.Metadata?.ToJObject());
|
||||
var additionalData = metaData
|
||||
.Where(dict => !InvoiceAdditionalDataExclude.Contains(dict.Key))
|
||||
.ToDictionary(dict => dict.Key, dict => dict.Value);
|
||||
|
||||
// Split receipt data into cart and additional data
|
||||
if (additionalData.TryGetValue("receiptData", out object? combinedReceiptData))
|
||||
{
|
||||
var receiptData = new Dictionary<string, object>((Dictionary<string, object>)combinedReceiptData, StringComparer.OrdinalIgnoreCase);
|
||||
string[] cartKeys = ["cart", "subtotal", "discount", "tip", "total"];
|
||||
// extract cart data and lowercase keys to handle data uniformly in PosData partial
|
||||
if (receiptData.Keys.Any(key => cartKeys.Contains(key.ToLowerInvariant())))
|
||||
{
|
||||
vm.CartData = new Dictionary<string, object>();
|
||||
foreach (var key in cartKeys)
|
||||
{
|
||||
if (!receiptData.ContainsKey(key)) continue;
|
||||
// add it to cart data and remove it from the general data
|
||||
vm.CartData.Add(key.ToLowerInvariant(), receiptData[key]);
|
||||
receiptData.Remove(key);
|
||||
}
|
||||
}
|
||||
// assign the rest to additional data and remove empty values
|
||||
if (receiptData.Any())
|
||||
{
|
||||
vm.AdditionalData = receiptData
|
||||
.Where(x => !string.IsNullOrEmpty(x.Value.ToString()))
|
||||
.ToDictionary(x => x.Key, x => x.Value);
|
||||
}
|
||||
}
|
||||
|
||||
var payments = ViewPaymentRequestViewModel.PaymentRequestInvoicePayment.GetViewModels(i, _displayFormatter, _transactionLinkProviders);
|
||||
|
||||
vm.Amount = i.PaidAmount.Net;
|
||||
vm.Payments = receipt.ShowPayments is false ? null : payments;
|
||||
vm.AdditionalData = PosDataParser.ParsePosData(receiptData);
|
||||
|
||||
return View(print ? "InvoiceReceiptPrint" : "InvoiceReceipt", vm);
|
||||
}
|
||||
@ -341,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);
|
||||
}
|
||||
|
||||
@ -1073,7 +1105,7 @@ namespace BTCPayServer.Controllers
|
||||
storeIds.Add(i);
|
||||
}
|
||||
model.Search = fs;
|
||||
model.SearchText = fs.TextSearch;
|
||||
model.SearchText = fs.TextCombined;
|
||||
|
||||
var apps = await _appService.GetAllApps(GetUserId(), false, storeId);
|
||||
InvoiceQuery invoiceQuery = GetInvoiceQuery(fs, apps, timezoneOffset);
|
||||
|
@ -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)
|
||||
{
|
||||
@ -296,11 +299,11 @@ namespace BTCPayServer
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var createInvoice = new CreateInvoiceRequest()
|
||||
var createInvoice = new CreateInvoiceRequest
|
||||
{
|
||||
Amount = item?.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Topup? null: item?.Price,
|
||||
Amount = item?.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Topup ? null : item?.Price,
|
||||
Currency = currencyCode,
|
||||
Checkout = new InvoiceDataBase.CheckoutOptions()
|
||||
Checkout = new InvoiceDataBase.CheckoutOptions
|
||||
{
|
||||
RedirectURL = app.AppType switch
|
||||
{
|
||||
@ -312,6 +315,7 @@ namespace BTCPayServer
|
||||
AdditionalSearchTerms = new[] { AppService.GetAppSearchTerm(app) }
|
||||
};
|
||||
|
||||
var allowOverpay = item?.PriceType is not ViewPointOfSaleViewModel.ItemPriceType.Fixed;
|
||||
var invoiceMetadata = new InvoiceMetadata { OrderId = AppService.GetRandomOrderId() };
|
||||
if (item != null)
|
||||
{
|
||||
@ -326,7 +330,7 @@ namespace BTCPayServer
|
||||
store.GetStoreBlob(),
|
||||
createInvoice,
|
||||
additionalTags: new List<string> { AppService.GetAppInternalTag(appId) },
|
||||
allowOverpay: false);
|
||||
allowOverpay: allowOverpay);
|
||||
}
|
||||
|
||||
public class EditLightningAddressVM
|
||||
@ -495,7 +499,7 @@ namespace BTCPayServer
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<IActionResult> GetLNURLRequest(
|
||||
public async Task<IActionResult> GetLNURLRequest(
|
||||
string cryptoCode,
|
||||
Data.StoreData store,
|
||||
Data.StoreBlob blob,
|
||||
@ -522,7 +526,9 @@ namespace BTCPayServer
|
||||
return this.CreateAPIError(null, e.Message);
|
||||
}
|
||||
lnurlRequest = await CreateLNUrlRequestFromInvoice(cryptoCode, i, store, blob, lnurlRequest, lnUrlMetadata, allowOverpay);
|
||||
return lnurlRequest is null ? NotFound() : Ok(lnurlRequest);
|
||||
return lnurlRequest is null
|
||||
? BadRequest(new LNUrlStatusResponse { Status = "ERROR", Reason = "Unable to create LNURL request." })
|
||||
: Ok(lnurlRequest);
|
||||
}
|
||||
|
||||
private async Task<LNURLPayRequest> CreateLNUrlRequestFromInvoice(
|
||||
|
@ -277,6 +277,10 @@ namespace BTCPayServer.Controllers
|
||||
if (FormDataService.Validate(form, ModelState))
|
||||
{
|
||||
prBlob.FormResponse = FormDataService.GetValues(form);
|
||||
if(string.IsNullOrEmpty(prBlob.Email) && form.GetFieldByFullName("buyerEmail") is { } emailField)
|
||||
{
|
||||
prBlob.Email = emailField.Value;
|
||||
}
|
||||
result.SetBlob(prBlob);
|
||||
await _PaymentRequestRepository.CreateOrUpdatePaymentRequest(result);
|
||||
return RedirectToAction("PayPaymentRequest", new { payReqId });
|
||||
|
@ -227,6 +227,7 @@ namespace BTCPayServer.Controllers
|
||||
var supported = ppBlob.SupportedPaymentMethods;
|
||||
PaymentMethodId paymentMethodId = null;
|
||||
IClaimDestination destination = null;
|
||||
string error = null;
|
||||
if (string.IsNullOrEmpty(vm.SelectedPaymentMethod))
|
||||
{
|
||||
foreach (var pmId in supported)
|
||||
@ -235,6 +236,7 @@ namespace BTCPayServer.Controllers
|
||||
(IClaimDestination dst, string err) = handler == null
|
||||
? (null, "No payment handler found for this payment method")
|
||||
: await handler.ParseAndValidateClaimDestination(pmId, vm.Destination, ppBlob, cancellationToken);
|
||||
error = err;
|
||||
if (dst is not null && err is null)
|
||||
{
|
||||
paymentMethodId = pmId;
|
||||
@ -247,12 +249,15 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
paymentMethodId = supported.FirstOrDefault(id => vm.SelectedPaymentMethod == id.ToString());
|
||||
var payoutHandler = paymentMethodId is null ? null : _payoutHandlers.FindPayoutHandler(paymentMethodId);
|
||||
destination = payoutHandler is null ? null : (await payoutHandler.ParseAndValidateClaimDestination(paymentMethodId, vm.Destination, ppBlob, cancellationToken)).destination;
|
||||
if (payoutHandler is not null)
|
||||
{
|
||||
(destination, error) = await payoutHandler.ParseAndValidateClaimDestination(paymentMethodId, vm.Destination, ppBlob, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
if (destination is null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.Destination), "Invalid destination or payment method");
|
||||
ModelState.AddModelError(nameof(vm.Destination), error ?? "Invalid destination or payment method");
|
||||
return await ViewPullPayment(pullPaymentId);
|
||||
}
|
||||
|
||||
|
@ -64,7 +64,27 @@ public partial class UIReportsController
|
||||
decimal randomValue = ((decimal)rand.NextDouble() * range) + from;
|
||||
return decimal.Round(randomValue, precision);
|
||||
}
|
||||
|
||||
JObject GetFormattedAmount()
|
||||
{
|
||||
string? curr = null;
|
||||
decimal value = 0m;
|
||||
int offset = 0;
|
||||
while (curr is null)
|
||||
{
|
||||
curr = row[fi - 1 - offset]?.ToString();
|
||||
value = curr switch
|
||||
{
|
||||
"USD" or "EUR" or "CHF" => GenerateDecimal(30_000m, 60_000, 2),
|
||||
"JPY" => GenerateDecimal(400_0000m, 1000_0000m, 0),
|
||||
_ => 0.0m
|
||||
};
|
||||
if (value != 0.0m)
|
||||
break;
|
||||
curr = null;
|
||||
offset++;
|
||||
}
|
||||
return DisplayFormatter.ToFormattedAmount(value, curr);
|
||||
}
|
||||
var fiatCurrency = rand.NextSingle() > 0.2 ? "USD" : TakeOne("JPY", "EUR", "CHF");
|
||||
var cryptoCurrency = rand.NextSingle() > 0.2 ? "BTC" : TakeOne("LTC", "DOGE", "DASH");
|
||||
|
||||
@ -116,14 +136,11 @@ public partial class UIReportsController
|
||||
return Encoders.Hex.EncodeData(GenerateBytes(32));
|
||||
if (f.Name == "Rate")
|
||||
{
|
||||
var curr = row[fi - 1]?.ToString();
|
||||
var value = curr switch
|
||||
{
|
||||
"USD" or "EUR" or "CHF" => GenerateDecimal(30_000m, 60_000, 2),
|
||||
"JPY" => GenerateDecimal(400_0000m, 1000_0000m, 0),
|
||||
_ => GenerateDecimal(30_000m, 60_000, 2)
|
||||
};
|
||||
return DisplayFormatter.ToFormattedAmount(value, curr);
|
||||
return GetFormattedAmount();
|
||||
}
|
||||
if (f.Type == "amount")
|
||||
{
|
||||
return GetFormattedAmount();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
@ -10,10 +10,9 @@ using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.WalletViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.PayoutProcessors;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@ -23,7 +22,6 @@ using Microsoft.EntityFrameworkCore;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using MarkPayoutRequest = BTCPayServer.HostedServices.MarkPayoutRequest;
|
||||
using PayoutData = BTCPayServer.Data.PayoutData;
|
||||
using PullPaymentData = BTCPayServer.Data.PullPaymentData;
|
||||
using StoreData = BTCPayServer.Data.StoreData;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
@ -40,6 +38,8 @@ namespace BTCPayServer.Controllers
|
||||
private readonly ApplicationDbContextFactory _dbContextFactory;
|
||||
private readonly BTCPayNetworkJsonSerializerSettings _jsonSerializerSettings;
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly PayoutProcessorService _payoutProcessorService;
|
||||
private readonly IEnumerable<IPayoutProcessorFactory> _payoutProcessorFactories;
|
||||
|
||||
public StoreData CurrentStore
|
||||
{
|
||||
@ -55,6 +55,8 @@ namespace BTCPayServer.Controllers
|
||||
DisplayFormatter displayFormatter,
|
||||
PullPaymentHostedService pullPaymentHostedService,
|
||||
ApplicationDbContextFactory dbContextFactory,
|
||||
PayoutProcessorService payoutProcessorService,
|
||||
IEnumerable<IPayoutProcessorFactory> payoutProcessorFactories,
|
||||
BTCPayNetworkJsonSerializerSettings jsonSerializerSettings,
|
||||
IAuthorizationService authorizationService)
|
||||
{
|
||||
@ -66,8 +68,10 @@ namespace BTCPayServer.Controllers
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_jsonSerializerSettings = jsonSerializerSettings;
|
||||
_authorizationService = authorizationService;
|
||||
_payoutProcessorService = payoutProcessorService;
|
||||
_payoutProcessorFactories = payoutProcessorFactories;
|
||||
}
|
||||
|
||||
|
||||
[HttpGet("stores/{storeId}/pull-payments/new")]
|
||||
[Authorize(Policy = Policies.CanCreateNonApprovedPullPayments, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> NewPullPayment(string storeId)
|
||||
@ -287,6 +291,7 @@ namespace BTCPayServer.Controllers
|
||||
return NotFound();
|
||||
|
||||
vm.PaymentMethods = await _payoutHandlers.GetSupportedPaymentMethods(HttpContext.GetStoreData());
|
||||
vm.HasPayoutProcessor = await HasPayoutProcessor(storeId, vm.PaymentMethodId);
|
||||
var paymentMethodId = PaymentMethodId.Parse(vm.PaymentMethodId);
|
||||
var handler = _payoutHandlers
|
||||
.FindPayoutHandler(paymentMethodId);
|
||||
@ -370,7 +375,7 @@ namespace BTCPayServer.Controllers
|
||||
break;
|
||||
}
|
||||
|
||||
if (command == "approve-pay")
|
||||
if (command == "approve-pay" && !vm.HasPayoutProcessor)
|
||||
{
|
||||
goto case "pay";
|
||||
}
|
||||
@ -486,16 +491,18 @@ namespace BTCPayServer.Controllers
|
||||
return RedirectToAction(nameof(UIStoresController.Index), "UIStores", new { storeId });
|
||||
}
|
||||
|
||||
paymentMethodId ??= paymentMethods.First().ToString();
|
||||
var vm = this.ParseListQuery(new PayoutsModel
|
||||
{
|
||||
PaymentMethods = paymentMethods,
|
||||
PaymentMethodId = paymentMethodId ?? paymentMethods.First().ToString(),
|
||||
PaymentMethodId = paymentMethodId,
|
||||
PullPaymentId = pullPaymentId,
|
||||
PayoutState = payoutState,
|
||||
Skip = skip,
|
||||
Count = count
|
||||
Count = count,
|
||||
Payouts = new List<PayoutsModel.PayoutModel>(),
|
||||
HasPayoutProcessor = await HasPayoutProcessor(storeId, paymentMethodId)
|
||||
});
|
||||
vm.Payouts = new List<PayoutsModel.PayoutModel>();
|
||||
await using var ctx = _dbContextFactory.CreateContext();
|
||||
var payoutRequest =
|
||||
ctx.Payouts.Where(p => p.StoreDataId == storeId && (p.PullPaymentDataId == null || !p.PullPaymentData.Archived));
|
||||
@ -577,5 +584,13 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
private async Task<bool> HasPayoutProcessor(string storeId, string paymentMethodId)
|
||||
{
|
||||
var pmId = PaymentMethodId.Parse(paymentMethodId);
|
||||
var processors = await _payoutProcessorService.GetProcessors(
|
||||
new PayoutProcessorService.PayoutProcessorQuery { Stores = [storeId], PaymentMethods = [paymentMethodId] });
|
||||
return _payoutProcessorFactories.Any(factory => factory.GetSupportedPaymentMethods().Contains(pmId)) && processors.Any();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -151,13 +151,12 @@ namespace BTCPayServer.Controllers
|
||||
WalletId walletId, WalletPSBTViewModel vm, string command = null)
|
||||
{
|
||||
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode);
|
||||
var psbt = await vm.GetPSBT(network.NBitcoinNetwork);
|
||||
var psbt = await vm.GetPSBT(network.NBitcoinNetwork, ModelState);
|
||||
|
||||
vm.BackUrl ??= HttpContext.Request.GetTypedHeaders().Referer?.AbsolutePath;
|
||||
|
||||
if (psbt is null || vm.InvalidPSBT)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.PSBT), "Invalid PSBT");
|
||||
return View("WalletSigningOptions", new WalletSigningOptionsModel
|
||||
{
|
||||
SigningContext = vm.SigningContext,
|
||||
@ -241,10 +240,9 @@ namespace BTCPayServer.Controllers
|
||||
vm.NBXSeedAvailable = await CanUseHotWallet() && derivationSchemeSettings.IsHotWallet;
|
||||
vm.BackUrl ??= HttpContext.Request.GetTypedHeaders().Referer?.AbsolutePath;
|
||||
|
||||
var psbt = await vm.GetPSBT(network.NBitcoinNetwork);
|
||||
var psbt = await vm.GetPSBT(network.NBitcoinNetwork, ModelState);
|
||||
if (vm.InvalidPSBT)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.PSBT), "Invalid PSBT");
|
||||
return View(vm);
|
||||
}
|
||||
if (psbt is null)
|
||||
@ -477,7 +475,7 @@ namespace BTCPayServer.Controllers
|
||||
WalletId walletId, WalletPSBTViewModel vm, string command, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode);
|
||||
PSBT psbt = await vm.GetPSBT(network.NBitcoinNetwork);
|
||||
PSBT psbt = await vm.GetPSBT(network.NBitcoinNetwork, ModelState);
|
||||
if (vm.InvalidPSBT || psbt is null)
|
||||
{
|
||||
if (vm.InvalidPSBT)
|
||||
@ -637,16 +635,14 @@ namespace BTCPayServer.Controllers
|
||||
WalletId walletId, WalletPSBTCombineViewModel vm)
|
||||
{
|
||||
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode);
|
||||
var psbt = await vm.GetPSBT(network.NBitcoinNetwork);
|
||||
var psbt = await vm.GetPSBT(network.NBitcoinNetwork, ModelState);
|
||||
if (psbt == null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.PSBT), "Invalid PSBT");
|
||||
return View(vm);
|
||||
}
|
||||
var sourcePSBT = vm.GetSourcePSBT(network.NBitcoinNetwork);
|
||||
if (sourcePSBT == null)
|
||||
var sourcePSBT = vm.GetSourcePSBT(network.NBitcoinNetwork, ModelState);
|
||||
if (sourcePSBT is null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.OtherPSBT), "Invalid PSBT");
|
||||
return View(vm);
|
||||
}
|
||||
sourcePSBT = sourcePSBT.Combine(psbt);
|
||||
|
@ -738,7 +738,7 @@ namespace BTCPayServer.Controllers
|
||||
foreach (var transactionOutput in vm.Outputs.Where(output => output.Labels?.Any() is true))
|
||||
{
|
||||
var labels = transactionOutput.Labels.Where(s => !string.IsNullOrWhiteSpace(s)).ToArray();
|
||||
var walletObjectAddress = new WalletObjectId(walletId, WalletObjectData.Types.Address, transactionOutput.DestinationAddress.ToLowerInvariant());
|
||||
var walletObjectAddress = new WalletObjectId(walletId, WalletObjectData.Types.Address, transactionOutput.DestinationAddress);
|
||||
var obj = await WalletRepository.GetWalletObject(walletObjectAddress);
|
||||
if (obj is null)
|
||||
{
|
||||
|
@ -11,7 +11,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike
|
||||
Bolt11 = bolt11 ?? throw new ArgumentNullException(nameof(bolt11));
|
||||
PaymentRequest = paymentRequest;
|
||||
PaymentHash = paymentRequest.Hash;
|
||||
Amount = paymentRequest.MinimumAmount.ToDecimal(LightMoneyUnit.BTC);
|
||||
Amount = paymentRequest.MinimumAmount.MilliSatoshi == LightMoney.Zero ? null: paymentRequest.MinimumAmount.ToDecimal(LightMoneyUnit.BTC);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
|
@ -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()
|
||||
|
@ -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"
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -37,11 +37,11 @@ namespace BTCPayServer.Filters
|
||||
}
|
||||
|
||||
// If we have an appId, we can redirect to the canonical domain
|
||||
if ((string)context.RouteContext.RouteData.Values["appId"] is { } appId && !req.IsOnion())
|
||||
if ((string)context.RouteContext.RouteData.Values["appId"] is { } appId)
|
||||
{
|
||||
var redirectDomain = mapping.FirstOrDefault(item => item.AppId == appId)?.Domain;
|
||||
// App is accessed via path, redirect to canonical domain
|
||||
if (!string.IsNullOrEmpty(redirectDomain) && req.Method != "POST" && !req.HasFormContentType)
|
||||
if (!string.IsNullOrEmpty(redirectDomain) && req.Method != "POST" && !req.HasFormContentType && !req.IsOnion())
|
||||
{
|
||||
var uri = new UriBuilder(req.Scheme, redirectDomain);
|
||||
if (req.Host.Port.HasValue)
|
||||
|
@ -7,11 +7,13 @@ using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Models.WalletViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Notifications;
|
||||
using BTCPayServer.Services.Notifications.Blobs;
|
||||
using BTCPayServer.Services.Rates;
|
||||
@ -47,7 +49,7 @@ namespace BTCPayServer.HostedServices
|
||||
public class PullPaymentHostedService : BaseAsyncService
|
||||
{
|
||||
private readonly string[] _lnurlSupportedCurrencies = { "BTC", "SATS" };
|
||||
|
||||
|
||||
public class CancelRequest
|
||||
{
|
||||
public CancelRequest(string pullPaymentId)
|
||||
@ -107,7 +109,23 @@ namespace BTCPayServer.HostedServices
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Task<string> CreatePullPayment(string storeId, CreatePullPaymentRequest request)
|
||||
{
|
||||
return CreatePullPayment(new CreatePullPayment()
|
||||
{
|
||||
StartsAt = request.StartsAt,
|
||||
ExpiresAt = request.ExpiresAt,
|
||||
Period = request.Period,
|
||||
BOLT11Expiration = request.BOLT11Expiration,
|
||||
Name = request.Name,
|
||||
Description = request.Description,
|
||||
Amount = request.Amount,
|
||||
Currency = request.Currency,
|
||||
StoreId = storeId,
|
||||
PaymentMethodIds = request.PaymentMethods.Select(p => PaymentMethodId.Parse(p)).ToArray(),
|
||||
AutoApproveClaims = request.AutoApproveClaims
|
||||
});
|
||||
}
|
||||
public async Task<string> CreatePullPayment(CreatePullPayment create)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(create);
|
||||
@ -263,7 +281,7 @@ namespace BTCPayServer.HostedServices
|
||||
|
||||
return await query.FirstOrDefaultAsync(data => data.Id == pullPaymentId);
|
||||
}
|
||||
|
||||
record TopUpRequest(string PullPaymentId, InvoiceEntity InvoiceEntity);
|
||||
class PayoutRequest
|
||||
{
|
||||
public PayoutRequest(TaskCompletionSource<ClaimRequest.ClaimResponse> completionSource,
|
||||
@ -323,10 +341,21 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
payoutHandler.StartBackgroundCheck(Subscribe);
|
||||
}
|
||||
|
||||
_eventAggregator.Subscribe<Events.InvoiceEvent>(TopUpInvoiceCore);
|
||||
return new[] { Loop() };
|
||||
}
|
||||
|
||||
private void TopUpInvoiceCore(InvoiceEvent evt)
|
||||
{
|
||||
if (evt.EventCode == InvoiceEventCode.Completed || evt.EventCode == InvoiceEventCode.MarkedCompleted)
|
||||
{
|
||||
foreach (var pullPaymentId in evt.Invoice.GetInternalTags("PULLPAY#"))
|
||||
{
|
||||
_Channel.Writer.TryWrite(new TopUpRequest(pullPaymentId, evt.Invoice));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Subscribe(params Type[] events)
|
||||
{
|
||||
foreach (Type @event in events)
|
||||
@ -339,6 +368,11 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
await foreach (var o in _Channel.Reader.ReadAllAsync())
|
||||
{
|
||||
if (o is TopUpRequest topUp)
|
||||
{
|
||||
await HandleTopUp(topUp);
|
||||
}
|
||||
|
||||
if (o is PayoutRequest req)
|
||||
{
|
||||
await HandleCreatePayout(req);
|
||||
@ -373,10 +407,44 @@ namespace BTCPayServer.HostedServices
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleTopUp(TopUpRequest topUp)
|
||||
{
|
||||
var pp = await this.GetPullPayment(topUp.PullPaymentId, false);
|
||||
var currency = pp.GetBlob().Currency;
|
||||
using var ctx = _dbContextFactory.CreateContext();
|
||||
|
||||
var payout = new Data.PayoutData()
|
||||
{
|
||||
Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(20)),
|
||||
Date = DateTimeOffset.UtcNow,
|
||||
State = PayoutState.Completed,
|
||||
PullPaymentDataId = pp.Id,
|
||||
PaymentMethodId = new PaymentMethodId("BTC", PaymentTypes.LightningLike).ToString(),
|
||||
Destination = null,
|
||||
StoreDataId = pp.StoreId
|
||||
};
|
||||
if (topUp.InvoiceEntity.Currency != currency ||
|
||||
currency is not ("SATS" or "BTC"))
|
||||
return;
|
||||
var paidAmount = topUp.InvoiceEntity.Price;
|
||||
var cryptoAmount = paidAmount;
|
||||
|
||||
var payoutBlob = new PayoutBlob()
|
||||
{
|
||||
CryptoAmount = -cryptoAmount,
|
||||
Amount = -paidAmount,
|
||||
Destination = topUp.InvoiceEntity.Id,
|
||||
Metadata = new JObject(),
|
||||
};
|
||||
payout.SetBlob(payoutBlob, _jsonSerializerSettings);
|
||||
await ctx.Payouts.AddAsync(payout);
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public bool SupportsLNURL(PullPaymentBlob blob)
|
||||
{
|
||||
var pms = blob.SupportedPaymentMethods.FirstOrDefault(id =>
|
||||
id.PaymentType == LightningPaymentType.Instance &&
|
||||
var pms = blob.SupportedPaymentMethods.FirstOrDefault(id =>
|
||||
id.PaymentType == LightningPaymentType.Instance &&
|
||||
_networkProvider.DefaultNetwork.CryptoCode == id.CryptoCode);
|
||||
return pms is not null && _lnurlSupportedCurrencies.Contains(blob.Currency);
|
||||
}
|
||||
@ -633,7 +701,7 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
Amount = claimed,
|
||||
Destination = req.ClaimRequest.Destination.ToString(),
|
||||
Metadata = req.ClaimRequest.Metadata?? new JObject(),
|
||||
Metadata = req.ClaimRequest.Metadata ?? new JObject(),
|
||||
};
|
||||
payout.SetBlob(payoutBlob, _jsonSerializerSettings);
|
||||
await ctx.Payouts.AddAsync(payout);
|
||||
@ -826,6 +894,10 @@ namespace BTCPayServer.HostedServices
|
||||
return time;
|
||||
}
|
||||
|
||||
public static string GetInternalTag(string id)
|
||||
{
|
||||
return $"PULLPAY#{id}";
|
||||
}
|
||||
|
||||
class InternalPayoutPaidRequest
|
||||
{
|
||||
@ -880,25 +952,25 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
null when destination.Amount is null && ppCurrency is null => ("Amount is not specified in destination or payout request", null),
|
||||
null when destination.Amount is null => (null, null),
|
||||
null when destination.Amount != null => (null,destination.Amount),
|
||||
not null when destination.Amount is null => (null,amount),
|
||||
null when destination.Amount != null => (null, destination.Amount),
|
||||
not null when destination.Amount is null => (null, amount),
|
||||
not null when destination.Amount != null && amount != destination.Amount &&
|
||||
destination.IsExplicitAmountMinimum &&
|
||||
payoutCurrency == "BTC" && ppCurrency == "SATS" &&
|
||||
new Money(amount.Value, MoneyUnit.Satoshi).ToUnit(MoneyUnit.BTC) < destination.Amount =>
|
||||
($"Amount is implied in both destination ({destination.Amount}) and payout request ({amount}), but the payout request amount is less than the destination amount",null),
|
||||
($"Amount is implied in both destination ({destination.Amount}) and payout request ({amount}), but the payout request amount is less than the destination amount", null),
|
||||
not null when destination.Amount != null && amount != destination.Amount &&
|
||||
destination.IsExplicitAmountMinimum &&
|
||||
!(payoutCurrency == "BTC" && ppCurrency == "SATS") &&
|
||||
amount < destination.Amount =>
|
||||
($"Amount is implied in both destination ({destination.Amount}) and payout request ({amount}), but the payout request amount is less than the destination amount",null),
|
||||
($"Amount is implied in both destination ({destination.Amount}) and payout request ({amount}), but the payout request amount is less than the destination amount", null),
|
||||
not null when destination.Amount != null && amount != destination.Amount &&
|
||||
!destination.IsExplicitAmountMinimum =>
|
||||
($"Amount is implied in destination ({destination.Amount}) that does not match the payout amount provided {amount})", null),
|
||||
_ => (null, amount)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
public static string GetErrorMessage(ClaimResult result)
|
||||
{
|
||||
switch (result)
|
||||
|
@ -304,6 +304,7 @@ namespace BTCPayServer.Hosting
|
||||
});
|
||||
services.TryAddSingleton<BTCPayNetworkProvider>();
|
||||
|
||||
services.AddExceptionHandler<PluginExceptionHandler>();
|
||||
services.TryAddSingleton<AppService>();
|
||||
services.AddTransient<PluginService>();
|
||||
services.AddSingleton<PluginHookService>();
|
||||
@ -347,6 +348,8 @@ namespace BTCPayServer.Hosting
|
||||
htmlSanitizer.AllowedAttributes.Add("webkitallowfullscreen");
|
||||
htmlSanitizer.AllowedAttributes.Add("allowfullscreen");
|
||||
htmlSanitizer.AllowedSchemes.Add("mailto");
|
||||
htmlSanitizer.AllowedSchemes.Add("bitcoin");
|
||||
htmlSanitizer.AllowedSchemes.Add("lightning");
|
||||
return htmlSanitizer;
|
||||
});
|
||||
|
||||
@ -385,6 +388,7 @@ namespace BTCPayServer.Hosting
|
||||
services.AddReportProvider<ProductsReportProvider>();
|
||||
services.AddReportProvider<PayoutsReportProvider>();
|
||||
services.AddReportProvider<LegacyInvoiceExportReportProvider>();
|
||||
services.AddReportProvider<RefundsReportProvider>();
|
||||
services.AddWebhooks();
|
||||
services.AddSingleton<BitcoinLikePayoutHandler>();
|
||||
services.AddSingleton<IPayoutHandler>(provider => provider.GetRequiredService<BitcoinLikePayoutHandler>());
|
||||
|
@ -293,6 +293,7 @@ namespace BTCPayServer.Hosting
|
||||
|
||||
app.UseStatusCodePagesWithReExecute("/errors/{0}");
|
||||
|
||||
app.UseExceptionHandler("/errors/{0}");
|
||||
app.UsePayServer();
|
||||
app.UseRouting();
|
||||
app.UseCors();
|
||||
|
@ -17,8 +17,9 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
public decimal Amount { get; set; }
|
||||
public DateTimeOffset Timestamp { get; set; }
|
||||
public Dictionary<string, object> AdditionalData { get; set; }
|
||||
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; }
|
||||
}
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ namespace BTCPayServer.Models.WalletViewModels
|
||||
public IEnumerable<PaymentMethodId> PaymentMethods { get; set; }
|
||||
public PayoutState PayoutState { get; set; }
|
||||
public string PullPaymentName { get; set; }
|
||||
public bool HasPayoutProcessor { get; set; }
|
||||
|
||||
public class PayoutModel
|
||||
{
|
||||
|
@ -2,6 +2,7 @@ using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Models.WalletViewModels
|
||||
@ -17,7 +18,7 @@ namespace BTCPayServer.Models.WalletViewModels
|
||||
public string BackUrl { get; set; }
|
||||
public string ReturnUrl { get; set; }
|
||||
|
||||
public PSBT GetSourcePSBT(Network network)
|
||||
public PSBT GetSourcePSBT(Network network, ModelStateDictionary modelState)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(OtherPSBT))
|
||||
{
|
||||
@ -25,12 +26,12 @@ namespace BTCPayServer.Models.WalletViewModels
|
||||
{
|
||||
return NBitcoin.PSBT.Parse(OtherPSBT, network);
|
||||
}
|
||||
catch
|
||||
{ }
|
||||
catch (Exception ex)
|
||||
{ modelState.AddModelError(nameof(OtherPSBT), ex.Message); }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
public async Task<PSBT> GetPSBT(Network network)
|
||||
public async Task<PSBT> GetPSBT(Network network, ModelStateDictionary modelState)
|
||||
{
|
||||
if (UploadedPSBTFile != null)
|
||||
{
|
||||
@ -45,8 +46,9 @@ namespace BTCPayServer.Models.WalletViewModels
|
||||
{
|
||||
return NBitcoin.PSBT.Load(bytes, network);
|
||||
}
|
||||
catch
|
||||
catch (FormatException ex)
|
||||
{
|
||||
modelState.AddModelError(nameof(UploadedPSBTFile), ex.Message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -56,8 +58,10 @@ namespace BTCPayServer.Models.WalletViewModels
|
||||
{
|
||||
return NBitcoin.PSBT.Parse(PSBT, network);
|
||||
}
|
||||
catch
|
||||
{ }
|
||||
catch (FormatException ex)
|
||||
{
|
||||
modelState.AddModelError(nameof(UploadedPSBTFile), ex.Message);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Models.WalletViewModels
|
||||
@ -35,9 +36,9 @@ namespace BTCPayServer.Models.WalletViewModels
|
||||
public IFormFile UploadedPSBTFile { get; set; }
|
||||
|
||||
|
||||
public async Task<PSBT> GetPSBT(Network network)
|
||||
public async Task<PSBT> GetPSBT(Network network, ModelStateDictionary modelState)
|
||||
{
|
||||
var psbt = await GetPSBTCore(network);
|
||||
var psbt = await GetPSBTCore(network, modelState);
|
||||
if (psbt != null)
|
||||
{
|
||||
Decoded = psbt.ToString();
|
||||
@ -52,7 +53,7 @@ namespace BTCPayServer.Models.WalletViewModels
|
||||
}
|
||||
public bool InvalidPSBT { get; set; }
|
||||
|
||||
async Task<PSBT> GetPSBTCore(Network network)
|
||||
async Task<PSBT> GetPSBTCore(Network network, ModelStateDictionary modelState)
|
||||
{
|
||||
if (UploadedPSBTFile != null)
|
||||
{
|
||||
@ -68,16 +69,20 @@ namespace BTCPayServer.Models.WalletViewModels
|
||||
}
|
||||
return NBitcoin.PSBT.Load(bytes, network);
|
||||
}
|
||||
catch (Exception)
|
||||
catch (Exception ex)
|
||||
{
|
||||
using var stream = new StreamReader(UploadedPSBTFile.OpenReadStream());
|
||||
PSBT = await stream.ReadToEndAsync();
|
||||
modelState.Remove(nameof(PSBT));
|
||||
modelState.AddModelError(nameof(PSBT), ex.Message);
|
||||
InvalidPSBT = true;
|
||||
}
|
||||
}
|
||||
if (SigningContext != null && !string.IsNullOrEmpty(SigningContext.PSBT))
|
||||
{
|
||||
PSBT = SigningContext.PSBT;
|
||||
modelState.Remove(nameof(PSBT));
|
||||
InvalidPSBT = false;
|
||||
}
|
||||
if (!string.IsNullOrEmpty(PSBT))
|
||||
{
|
||||
@ -86,8 +91,11 @@ namespace BTCPayServer.Models.WalletViewModels
|
||||
InvalidPSBT = false;
|
||||
return NBitcoin.PSBT.Parse(PSBT, network);
|
||||
}
|
||||
catch
|
||||
{ InvalidPSBT = true; }
|
||||
catch (Exception ex) when (!InvalidPSBT)
|
||||
{
|
||||
modelState.AddModelError(nameof(PSBT), ex.Message);
|
||||
InvalidPSBT = true;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
@ -3,10 +3,10 @@ using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Services.Altcoins.Monero.RPC.Models
|
||||
{
|
||||
public partial class SyncInfoResponse
|
||||
public partial class GetInfoResponse
|
||||
{
|
||||
[JsonProperty("height")] public long Height { get; set; }
|
||||
[JsonProperty("peers")] public List<Peer> Peers { get; set; }
|
||||
[JsonProperty("busy_syncing")] public bool BusySyncing { get; set; }
|
||||
[JsonProperty("status")] public string Status { get; set; }
|
||||
[JsonProperty("target_height")] public long? TargetHeight { get; set; }
|
||||
}
|
@ -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; }
|
||||
}
|
||||
}
|
@ -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; }
|
||||
}
|
||||
}
|
@ -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; }
|
||||
}
|
||||
}
|
52
BTCPayServer/Plugins/PluginExceptionHandler.cs
Normal file
52
BTCPayServer/Plugins/PluginExceptionHandler.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -1,87 +0,0 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Plugins.Shopify.ApiModels;
|
||||
|
||||
namespace BTCPayServer.Plugins.Shopify
|
||||
{
|
||||
public class OrderTransactionRegisterLogic
|
||||
{
|
||||
private readonly ShopifyApiClient _client;
|
||||
|
||||
public OrderTransactionRegisterLogic(ShopifyApiClient client)
|
||||
{
|
||||
_client = client;
|
||||
}
|
||||
|
||||
private static string[] _keywords = new[] { "bitcoin", "btc", "btcpayserver", "btcpay server" };
|
||||
public async Task<TransactionsCreateResp> Process(string orderId, string invoiceId, string currency, string amountCaptured, bool success)
|
||||
{
|
||||
currency = currency.ToUpperInvariant().Trim();
|
||||
var existingShopifyOrderTransactions = (await _client.TransactionsList(orderId)).transactions;
|
||||
|
||||
//if there isn't a record for btcpay payment gateway, abort
|
||||
var baseParentTransaction = existingShopifyOrderTransactions.FirstOrDefault(holder => _keywords.Any(a => holder.gateway.Contains(a, StringComparison.InvariantCultureIgnoreCase)));
|
||||
if (baseParentTransaction is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
//technically, this exploit should not be possible as we use internal invoice tags to verify that the invoice was created by our controlled, dedicated endpoint.
|
||||
if (currency.ToUpperInvariant().Trim() != baseParentTransaction.currency.ToUpperInvariant().Trim())
|
||||
{
|
||||
// because of parent_id present, currency will always be the one from parent transaction
|
||||
// malicious attacker could potentially exploit this by creating invoice
|
||||
// in different currency and paying that one, registering order on Shopify as paid
|
||||
// so if currency is supplied and is different from parent transaction currency we just won't register
|
||||
return null;
|
||||
}
|
||||
|
||||
var kind = "capture";
|
||||
var parentId = baseParentTransaction.id;
|
||||
var status = success ? "success" : "failure";
|
||||
//find all existing transactions recorded around this invoice id
|
||||
var existingShopifyOrderTransactionsOnSameInvoice =
|
||||
existingShopifyOrderTransactions.Where(holder => holder.authorization == invoiceId);
|
||||
|
||||
//filter out the successful ones
|
||||
var successfulActions =
|
||||
existingShopifyOrderTransactionsOnSameInvoice.Where(holder => holder.status == "success").ToArray();
|
||||
|
||||
//of the successful ones, get the ones we registered as a valid payment
|
||||
var successfulCaptures = successfulActions.Where(holder => holder.kind == "capture").ToArray();
|
||||
|
||||
//of the successful ones, get the ones we registered as a voiding of a previous successful payment
|
||||
var refunds = successfulActions.Where(holder => holder.kind == "refund").ToArray();
|
||||
|
||||
//if we are working with a non-success registration, but see that we have previously registered this invoice as a success, we switch to creating a "void" transaction, which in shopify terms is a refund.
|
||||
if (!success && successfulCaptures.Length > 0 && (successfulCaptures.Length - refunds.Length) > 0)
|
||||
{
|
||||
kind = "void";
|
||||
parentId = successfulCaptures.Last().id;
|
||||
status = "success";
|
||||
}
|
||||
//if we are working with a success registration, but can see that we have already had a successful transaction saved, get outta here
|
||||
else if (success && successfulCaptures.Length > 0 && (successfulCaptures.Length - refunds.Length) > 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var createTransaction = new TransactionsCreateReq
|
||||
{
|
||||
transaction = new TransactionsCreateReq.DataHolder
|
||||
{
|
||||
parent_id = parentId,
|
||||
currency = currency,
|
||||
amount = amountCaptured,
|
||||
kind = kind,
|
||||
gateway = "BTCPayServer",
|
||||
source = "external",
|
||||
authorization = invoiceId,
|
||||
status = status
|
||||
}
|
||||
};
|
||||
var createResp = await _client.TransactionCreate(orderId, createTransaction);
|
||||
return createResp;
|
||||
}
|
||||
}
|
||||
}
|
@ -35,10 +35,10 @@ namespace BTCPayServer.Plugins.Shopify
|
||||
}
|
||||
|
||||
private HttpRequestMessage CreateRequest(string shopName, HttpMethod method, string action,
|
||||
string relativeUrl = null)
|
||||
string relativeUrl = null, string apiVersion = "2020-07")
|
||||
{
|
||||
var url =
|
||||
$"https://{(shopName.Contains('.', StringComparison.InvariantCulture) ? shopName : $"{shopName}.myshopify.com")}/{relativeUrl ?? ("admin/api/2020-07/" + action)}";
|
||||
$"https://{(shopName.Contains('.', StringComparison.InvariantCulture) ? shopName : $"{shopName}.myshopify.com")}/{relativeUrl ?? ($"admin/api/{apiVersion}/" + action)}";
|
||||
var req = new HttpRequestMessage(method, url);
|
||||
return req;
|
||||
}
|
||||
@ -115,6 +115,15 @@ namespace BTCPayServer.Plugins.Shopify
|
||||
|
||||
return JObject.Parse(strResp)["order"].ToObject<ShopifyOrder>();
|
||||
}
|
||||
public async Task<ShopifyOrder> CancelOrder(string orderId)
|
||||
{
|
||||
var req = CreateRequest(_credentials.ShopName, HttpMethod.Post,
|
||||
$"orders/{orderId}/cancel.json?restock=true", null, "2024-04");
|
||||
|
||||
var strResp = await SendRequest(req);
|
||||
|
||||
return JObject.Parse(strResp)["order"].ToObject<ShopifyOrder>();
|
||||
}
|
||||
|
||||
public async Task<long> OrdersCount()
|
||||
{
|
||||
|
@ -1,116 +0,0 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Plugins.Shopify.Models;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace BTCPayServer.Plugins.Shopify
|
||||
{
|
||||
public class ShopifyOrderMarkerHostedService : EventHostedServiceBase
|
||||
{
|
||||
private readonly StoreRepository _storeRepository;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
|
||||
public ShopifyOrderMarkerHostedService(EventAggregator eventAggregator,
|
||||
StoreRepository storeRepository,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
Logs logs) : base(eventAggregator, logs)
|
||||
{
|
||||
_storeRepository = storeRepository;
|
||||
_httpClientFactory = httpClientFactory;
|
||||
}
|
||||
|
||||
public const string SHOPIFY_ORDER_ID_PREFIX = "shopify-";
|
||||
|
||||
protected override void SubscribeToEvents()
|
||||
{
|
||||
Subscribe<InvoiceEvent>();
|
||||
base.SubscribeToEvents();
|
||||
}
|
||||
|
||||
protected override async Task ProcessEvent(object evt, CancellationToken cancellationToken)
|
||||
{
|
||||
if (evt is InvoiceEvent invoiceEvent && !new[]
|
||||
{
|
||||
InvoiceEvent.Created, InvoiceEvent.ExpiredPaidPartial,
|
||||
InvoiceEvent.ReceivedPayment, InvoiceEvent.PaidInFull
|
||||
}.Contains(invoiceEvent.Name))
|
||||
{
|
||||
var invoice = invoiceEvent.Invoice;
|
||||
var shopifyOrderId = invoice.GetInternalTags(SHOPIFY_ORDER_ID_PREFIX).FirstOrDefault();
|
||||
if (shopifyOrderId != null)
|
||||
{
|
||||
if (new[] { InvoiceStatusLegacy.Invalid, InvoiceStatusLegacy.Expired }.Contains(invoice.GetInvoiceState()
|
||||
.Status) && invoice.ExceptionStatus != InvoiceExceptionStatus.None)
|
||||
{
|
||||
//you have failed us, customer
|
||||
|
||||
await RegisterTransaction(invoice, shopifyOrderId, false);
|
||||
}
|
||||
else if (new[] { InvoiceStatusLegacy.Complete, InvoiceStatusLegacy.Confirmed }.Contains(
|
||||
invoice.Status))
|
||||
{
|
||||
await RegisterTransaction(invoice, shopifyOrderId, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await base.ProcessEvent(evt, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task RegisterTransaction(InvoiceEntity invoice, string shopifyOrderId, bool success)
|
||||
{
|
||||
var storeData = await _storeRepository.FindStore(invoice.StoreId);
|
||||
var storeBlob = storeData.GetStoreBlob();
|
||||
|
||||
// ensure that store in question has shopify integration turned on
|
||||
// and that invoice's orderId has shopify specific prefix
|
||||
var settings = storeBlob.GetShopifySettings();
|
||||
if (settings?.IntegratedAt.HasValue == true)
|
||||
{
|
||||
var client = CreateShopifyApiClient(settings);
|
||||
if (!await client.OrderExists(shopifyOrderId))
|
||||
{
|
||||
// don't register transactions for orders that don't exist on shopify
|
||||
return;
|
||||
}
|
||||
|
||||
// if we got this far, we likely need to register this invoice's payment on Shopify
|
||||
// OrderTransactionRegisterLogic has check if transaction is already registered which is why we're passing invoice.Id
|
||||
try
|
||||
{
|
||||
var logic = new OrderTransactionRegisterLogic(client);
|
||||
var resp = await logic.Process(shopifyOrderId, invoice.Id, invoice.Currency,
|
||||
invoice.Price.ToString(CultureInfo.InvariantCulture), success);
|
||||
if (resp != null)
|
||||
{
|
||||
Logs.PayServer.LogInformation($"Registered order transaction {invoice.Price}{invoice.Currency} on Shopify. " +
|
||||
$"Triggered by invoiceId: {invoice.Id}, Shopify orderId: {shopifyOrderId}, Success: {success}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.PayServer.LogError(ex,
|
||||
$"Shopify error while trying to register order transaction. " +
|
||||
$"Triggered by invoiceId: {invoice.Id}, Shopify orderId: {shopifyOrderId}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private ShopifyApiClient CreateShopifyApiClient(ShopifySettings shopify)
|
||||
{
|
||||
return new ShopifyApiClient(_httpClientFactory, shopify.CreateShopifyApiCredentials());
|
||||
}
|
||||
}
|
||||
}
|
@ -14,7 +14,8 @@ namespace BTCPayServer.Plugins.Shopify
|
||||
|
||||
public override void Execute(IServiceCollection applicationBuilder)
|
||||
{
|
||||
applicationBuilder.AddSingleton<IHostedService, ShopifyOrderMarkerHostedService>();
|
||||
applicationBuilder.AddSingleton<ShopifyService>();
|
||||
applicationBuilder.AddSingleton<IHostedService, ShopifyService>(provider => provider.GetRequiredService<ShopifyService>());
|
||||
applicationBuilder.AddSingleton<IUIExtension>(new UIExtension("Shopify/NavExtension", "header-nav"));
|
||||
base.Execute(applicationBuilder);
|
||||
}
|
||||
|
234
BTCPayServer/Plugins/Shopify/ShopifyService.cs
Normal file
234
BTCPayServer/Plugins/Shopify/ShopifyService.cs
Normal file
@ -0,0 +1,234 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Plugins.Shopify.ApiModels;
|
||||
using BTCPayServer.Plugins.Shopify.Models;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace BTCPayServer.Plugins.Shopify
|
||||
{
|
||||
public class ShopifyService : EventHostedServiceBase
|
||||
{
|
||||
private readonly StoreRepository _storeRepository;
|
||||
private readonly InvoiceRepository _invoiceRepository;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
|
||||
public ShopifyService(EventAggregator eventAggregator,
|
||||
StoreRepository storeRepository,
|
||||
InvoiceRepository invoiceRepository,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
Logs logs) : base(eventAggregator, logs)
|
||||
{
|
||||
_storeRepository = storeRepository;
|
||||
_invoiceRepository = invoiceRepository;
|
||||
_httpClientFactory = httpClientFactory;
|
||||
}
|
||||
|
||||
public const string SHOPIFY_ORDER_ID_PREFIX = "shopify-";
|
||||
|
||||
protected override void SubscribeToEvents()
|
||||
{
|
||||
Subscribe<InvoiceEvent>();
|
||||
base.SubscribeToEvents();
|
||||
}
|
||||
|
||||
protected override async Task ProcessEvent(object evt, CancellationToken cancellationToken)
|
||||
{
|
||||
if (evt is InvoiceEvent invoiceEvent && new[]
|
||||
{
|
||||
InvoiceEvent.MarkedCompleted,
|
||||
InvoiceEvent.MarkedInvalid,
|
||||
InvoiceEvent.Expired,
|
||||
InvoiceEvent.Confirmed,
|
||||
InvoiceEvent.Completed
|
||||
}.Contains(invoiceEvent.Name))
|
||||
{
|
||||
var invoice = invoiceEvent.Invoice;
|
||||
var shopifyOrderId = invoice.GetInternalTags(SHOPIFY_ORDER_ID_PREFIX).FirstOrDefault();
|
||||
if (shopifyOrderId != null)
|
||||
{
|
||||
var success = invoice.Status.ToModernStatus() switch
|
||||
{
|
||||
InvoiceStatus.Settled => true,
|
||||
InvoiceStatus.Invalid or InvoiceStatus.Expired => false,
|
||||
_ => (bool?)null
|
||||
};
|
||||
|
||||
if (success.HasValue)
|
||||
await RegisterTransaction(invoice, shopifyOrderId, success.Value);
|
||||
}
|
||||
}
|
||||
|
||||
await base.ProcessEvent(evt, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task RegisterTransaction(InvoiceEntity invoice, string shopifyOrderId, bool success)
|
||||
{
|
||||
var storeData = await _storeRepository.FindStore(invoice.StoreId);
|
||||
var storeBlob = storeData.GetStoreBlob();
|
||||
|
||||
// ensure that store in question has shopify integration turned on
|
||||
// and that invoice's orderId has shopify specific prefix
|
||||
var settings = storeBlob.GetShopifySettings();
|
||||
if (settings?.IntegratedAt.HasValue == true)
|
||||
{
|
||||
var client = CreateShopifyApiClient(settings);
|
||||
if (!await client.OrderExists(shopifyOrderId))
|
||||
{
|
||||
// don't register transactions for orders that don't exist on shopify
|
||||
return;
|
||||
}
|
||||
|
||||
// if we got this far, we likely need to register this invoice's payment on Shopify
|
||||
// OrderTransactionRegisterLogic has check if transaction is already registered which is why we're passing invoice.Id
|
||||
try
|
||||
{
|
||||
var resp = await Process(client, shopifyOrderId, invoice.Id, invoice.Currency,
|
||||
invoice.Price.ToString(CultureInfo.InvariantCulture), success);
|
||||
if (resp != null)
|
||||
{
|
||||
await _invoiceRepository.AddInvoiceLogs(invoice.Id, resp);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.PayServer.LogError(ex,
|
||||
$"Shopify error while trying to register order transaction. " +
|
||||
$"Triggered by invoiceId: {invoice.Id}, Shopify orderId: {shopifyOrderId}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private ShopifyApiClient CreateShopifyApiClient(ShopifySettings shopify)
|
||||
{
|
||||
return new ShopifyApiClient(_httpClientFactory, shopify.CreateShopifyApiCredentials());
|
||||
}
|
||||
|
||||
|
||||
private static string[] _keywords = new[] {"bitcoin", "btc", "btcpayserver", "btcpay server"};
|
||||
|
||||
public async Task<InvoiceLogs> Process(ShopifyApiClient client, string orderId, string invoiceId,
|
||||
string currency, string amountCaptured, bool success)
|
||||
{
|
||||
var result = new InvoiceLogs();
|
||||
currency = currency.ToUpperInvariant().Trim();
|
||||
var existingShopifyOrderTransactions = (await client.TransactionsList(orderId)).transactions;
|
||||
|
||||
//if there isn't a record for btcpay payment gateway, abort
|
||||
var baseParentTransaction = existingShopifyOrderTransactions.FirstOrDefault(holder =>
|
||||
_keywords.Any(a => holder.gateway.Contains(a, StringComparison.InvariantCultureIgnoreCase)));
|
||||
if (baseParentTransaction is null)
|
||||
{
|
||||
result.Write("Couldn't find the order on Shopify.", InvoiceEventData.EventSeverity.Error);
|
||||
return result;
|
||||
}
|
||||
|
||||
//technically, this exploit should not be possible as we use internal invoice tags to verify that the invoice was created by our controlled, dedicated endpoint.
|
||||
if (currency.ToUpperInvariant().Trim() != baseParentTransaction.currency.ToUpperInvariant().Trim())
|
||||
{
|
||||
// because of parent_id present, currency will always be the one from parent transaction
|
||||
// malicious attacker could potentially exploit this by creating invoice
|
||||
// in different currency and paying that one, registering order on Shopify as paid
|
||||
// so if currency is supplied and is different from parent transaction currency we just won't register
|
||||
result.Write("Currency mismatch on Shopify.", InvoiceEventData.EventSeverity.Error);
|
||||
return result;
|
||||
}
|
||||
|
||||
var kind = "capture";
|
||||
var parentId = baseParentTransaction.id;
|
||||
var status = success ? "success" : "failure";
|
||||
//find all existing transactions recorded around this invoice id
|
||||
var existingShopifyOrderTransactionsOnSameInvoice =
|
||||
existingShopifyOrderTransactions.Where(holder => holder.authorization == invoiceId);
|
||||
|
||||
//filter out the successful ones
|
||||
var successfulActions =
|
||||
existingShopifyOrderTransactionsOnSameInvoice.Where(holder => holder.status == "success").ToArray();
|
||||
|
||||
//of the successful ones, get the ones we registered as a valid payment
|
||||
var successfulCaptures = successfulActions.Where(holder => holder.kind == "capture").ToArray();
|
||||
|
||||
//of the successful ones, get the ones we registered as a voiding of a previous successful payment
|
||||
var refunds = successfulActions.Where(holder => holder.kind == "refund").ToArray();
|
||||
|
||||
//if we are working with a non-success registration, but see that we have previously registered this invoice as a success, we switch to creating a "void" transaction, which in shopify terms is a refund.
|
||||
if (!success && successfulCaptures.Length > 0 && (successfulCaptures.Length - refunds.Length) > 0)
|
||||
{
|
||||
kind = "void";
|
||||
parentId = successfulCaptures.Last().id;
|
||||
status = "success";
|
||||
result.Write(
|
||||
"A transaction was previously recorded against the Shopify order. Creating a void transaction.",
|
||||
InvoiceEventData.EventSeverity.Warning);
|
||||
}
|
||||
else if (!success)
|
||||
{
|
||||
kind = "void";
|
||||
status = "success";
|
||||
result.Write("Attempting to void the payment on Shopify order due to failure in payment.",
|
||||
InvoiceEventData.EventSeverity.Warning);
|
||||
}
|
||||
//if we are working with a success registration, but can see that we have already had a successful transaction saved, get outta here
|
||||
else if (success && successfulCaptures.Length > 0 && (successfulCaptures.Length - refunds.Length) > 0)
|
||||
{
|
||||
result.Write("A transaction was previously recorded against the Shopify order. Skipping.",
|
||||
InvoiceEventData.EventSeverity.Warning);
|
||||
return result;
|
||||
}
|
||||
|
||||
var createTransaction = new TransactionsCreateReq
|
||||
{
|
||||
transaction = new TransactionsCreateReq.DataHolder
|
||||
{
|
||||
parent_id = parentId,
|
||||
currency = currency,
|
||||
amount = amountCaptured,
|
||||
kind = kind,
|
||||
gateway = "BTCPayServer",
|
||||
source = "external",
|
||||
authorization = invoiceId,
|
||||
status = status
|
||||
}
|
||||
};
|
||||
var createResp = await client.TransactionCreate(orderId, createTransaction);
|
||||
|
||||
if (createResp.transaction is null)
|
||||
{
|
||||
result.Write("Failed to register the transaction on Shopify.", InvoiceEventData.EventSeverity.Error);
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Write(
|
||||
$"Successfully registered the transaction on Shopify. tx status:{createResp.transaction.status}, kind: {createResp.transaction.kind}, order id:{createResp.transaction.order_id}",
|
||||
InvoiceEventData.EventSeverity.Info);
|
||||
}
|
||||
|
||||
if (!success)
|
||||
{
|
||||
try
|
||||
{
|
||||
await client.CancelOrder(orderId);
|
||||
result.Write("Cancelling the Shopify order.", InvoiceEventData.EventSeverity.Warning);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
result.Write($"Failed to cancel the Shopify order. {e.Message}",
|
||||
InvoiceEventData.EventSeverity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
@ -34,6 +34,7 @@ namespace BTCPayServer.Plugins.Shopify
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public class UIShopifyController : Controller
|
||||
{
|
||||
private readonly ShopifyService _shopifyService;
|
||||
private readonly BTCPayServerEnvironment _btcPayServerEnvironment;
|
||||
private readonly IOptions<BTCPayServerOptions> _btcPayServerOptions;
|
||||
private readonly IWebHostEnvironment _webHostEnvironment;
|
||||
@ -43,7 +44,9 @@ namespace BTCPayServer.Plugins.Shopify
|
||||
private readonly IJsonHelper _jsonHelper;
|
||||
private readonly IHttpClientFactory _clientFactory;
|
||||
|
||||
public UIShopifyController(BTCPayServerEnvironment btcPayServerEnvironment,
|
||||
public UIShopifyController(
|
||||
ShopifyService shopifyService,
|
||||
BTCPayServerEnvironment btcPayServerEnvironment,
|
||||
IOptions<BTCPayServerOptions> btcPayServerOptions,
|
||||
IWebHostEnvironment webHostEnvironment,
|
||||
StoreRepository storeRepository,
|
||||
@ -52,6 +55,7 @@ namespace BTCPayServer.Plugins.Shopify
|
||||
IJsonHelper jsonHelper,
|
||||
IHttpClientFactory clientFactory)
|
||||
{
|
||||
_shopifyService = shopifyService;
|
||||
_btcPayServerEnvironment = btcPayServerEnvironment;
|
||||
_btcPayServerOptions = btcPayServerOptions;
|
||||
_webHostEnvironment = webHostEnvironment;
|
||||
@ -106,14 +110,14 @@ namespace BTCPayServer.Plugins.Shopify
|
||||
public async Task<IActionResult> ShopifyInvoiceEndpoint(
|
||||
string storeId, string orderId, decimal amount, bool checkOnly = false)
|
||||
{
|
||||
var shopifySearchTerm = $"{ShopifyOrderMarkerHostedService.SHOPIFY_ORDER_ID_PREFIX}{orderId}";
|
||||
var shopifySearchTerm = $"{ShopifyService.SHOPIFY_ORDER_ID_PREFIX}{orderId}";
|
||||
var matchedExistingInvoices = await _invoiceRepository.GetInvoices(new InvoiceQuery()
|
||||
{
|
||||
TextSearch = shopifySearchTerm,
|
||||
StoreId = new[] { storeId }
|
||||
});
|
||||
matchedExistingInvoices = matchedExistingInvoices.Where(entity =>
|
||||
entity.GetInternalTags(ShopifyOrderMarkerHostedService.SHOPIFY_ORDER_ID_PREFIX)
|
||||
entity.GetInternalTags(ShopifyService.SHOPIFY_ORDER_ID_PREFIX)
|
||||
.Any(s => s == orderId))
|
||||
.ToArray();
|
||||
|
||||
@ -155,7 +159,7 @@ namespace BTCPayServer.Plugins.Shopify
|
||||
if (client != null && order?.FinancialStatus == "pending" &&
|
||||
firstInvoiceSettled.Status != InvoiceStatusLegacy.Paid)
|
||||
{
|
||||
await new OrderTransactionRegisterLogic(client).Process(orderId, firstInvoiceSettled.Id,
|
||||
await _shopifyService.Process(client, orderId, firstInvoiceSettled.Id,
|
||||
firstInvoiceSettled.Currency,
|
||||
firstInvoiceSettled.Price.ToString(CultureInfo.InvariantCulture), true);
|
||||
order = await client.GetOrder(orderId);
|
||||
|
@ -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/"
|
||||
}
|
||||
|
@ -9,7 +9,8 @@ namespace BTCPayServer
|
||||
{
|
||||
private const char FilterSeparator = ',';
|
||||
private const char ValueSeparator = ':';
|
||||
|
||||
private static readonly string[] StripFilters = ["status", "exceptionstatus", "unusual", "includearchived", "appid", "startdate", "enddate"];
|
||||
|
||||
private readonly string _originalString;
|
||||
private readonly int _timezoneOffset;
|
||||
|
||||
@ -27,12 +28,18 @@ namespace BTCPayServer
|
||||
.Where(kv => kv.Length == 2)
|
||||
.Select(kv => new KeyValuePair<string, string>(UnifyKey(kv[0]), kv[1]))
|
||||
.ToMultiValueDictionary(o => o.Key, o => o.Value);
|
||||
|
||||
var val = splitted.FirstOrDefault(a => a.IndexOf(ValueSeparator, StringComparison.OrdinalIgnoreCase) == -1);
|
||||
TextSearch = val != null ? val.Trim() : string.Empty;
|
||||
// combine raw search term and filters which don't have a special UI (e.g. orderid)
|
||||
var textFilters = Filters
|
||||
.Where(f => !StripFilters.Contains(f.Key))
|
||||
.Select(f => string.Join(FilterSeparator, f.Value.Select(v => $"{f.Key}{ValueSeparator}{v}"))).ToList();
|
||||
TextFilters = textFilters.Any() ? string.Join(FilterSeparator, textFilters) : null;
|
||||
TextSearch = splitted.FirstOrDefault(a => a.IndexOf(ValueSeparator, StringComparison.OrdinalIgnoreCase) == -1)?.Trim();
|
||||
}
|
||||
|
||||
public string TextSearch { get; private set; }
|
||||
public string TextFilters { get; private set; }
|
||||
|
||||
public string TextCombined => string.Join(FilterSeparator, new []{ TextFilters, TextSearch }.Where(x => !string.IsNullOrEmpty(x)));
|
||||
|
||||
public MultiValueDictionary<string, string> Filters { get; }
|
||||
|
||||
@ -82,9 +89,10 @@ namespace BTCPayServer
|
||||
|
||||
public string WithoutSearchText()
|
||||
{
|
||||
return string.IsNullOrEmpty(TextSearch)
|
||||
? Finalize(ToString())
|
||||
: Finalize(ToString()).Replace(TextSearch, string.Empty);
|
||||
var txt = ToString();
|
||||
if (!string.IsNullOrEmpty(TextSearch)) txt = Finalize(txt.Replace(TextSearch, string.Empty));
|
||||
if (!string.IsNullOrEmpty(TextFilters)) txt = Finalize(txt.Replace(TextFilters, string.Empty));
|
||||
return Finalize(txt).Trim();
|
||||
}
|
||||
|
||||
public string[] GetFilterArray(string key)
|
||||
@ -144,7 +152,7 @@ namespace BTCPayServer
|
||||
|
||||
private static string Finalize(string str)
|
||||
{
|
||||
var value = str.TrimStart(FilterSeparator).TrimEnd(FilterSeparator);
|
||||
var value = str.Trim().TrimStart(FilterSeparator).TrimEnd(FilterSeparator);
|
||||
return string.IsNullOrEmpty(value) ? " " : value;
|
||||
}
|
||||
}
|
||||
|
@ -59,12 +59,12 @@ namespace BTCPayServer.Services.Altcoins.Monero.Services
|
||||
try
|
||||
{
|
||||
var daemonResult =
|
||||
await daemonRpcClient.SendCommandAsync<JsonRpcClient.NoRequestModel, SyncInfoResponse>("sync_info",
|
||||
await daemonRpcClient.SendCommandAsync<JsonRpcClient.NoRequestModel, GetInfoResponse>("get_info",
|
||||
JsonRpcClient.NoRequestModel.Instance);
|
||||
summary.TargetHeight = daemonResult.TargetHeight.GetValueOrDefault(0);
|
||||
summary.CurrentHeight = daemonResult.Height;
|
||||
summary.TargetHeight = summary.TargetHeight == 0 ? summary.CurrentHeight : summary.TargetHeight;
|
||||
summary.Synced = daemonResult.Height >= summary.TargetHeight && summary.CurrentHeight > 0;
|
||||
summary.Synced = !daemonResult.BusySyncing;
|
||||
summary.UpdatedAt = DateTime.UtcNow;
|
||||
summary.DaemonAvailable = true;
|
||||
}
|
||||
|
@ -104,14 +104,19 @@ namespace BTCPayServer.Services.Altcoins.Monero.UI
|
||||
new SelectListItem(
|
||||
$"{account.AccountIndex} - {(string.IsNullOrEmpty(account.Label) ? "No label" : account.Label)}",
|
||||
account.AccountIndex.ToString(CultureInfo.InvariantCulture)));
|
||||
var settlementThresholdChoice = settings.InvoiceSettledConfirmationThreshold switch
|
||||
|
||||
var settlementThresholdChoice = MoneroLikeSettlementThresholdChoice.StoreSpeedPolicy;
|
||||
if (settings != null && settings.InvoiceSettledConfirmationThreshold is { } confirmations)
|
||||
{
|
||||
null => MoneroLikeSettlementThresholdChoice.StoreSpeedPolicy,
|
||||
0 => MoneroLikeSettlementThresholdChoice.ZeroConfirmation,
|
||||
1 => MoneroLikeSettlementThresholdChoice.AtLeastOne,
|
||||
10 => MoneroLikeSettlementThresholdChoice.AtLeastTen,
|
||||
_ => MoneroLikeSettlementThresholdChoice.Custom
|
||||
};
|
||||
settlementThresholdChoice = confirmations switch
|
||||
{
|
||||
0 => MoneroLikeSettlementThresholdChoice.ZeroConfirmation,
|
||||
1 => MoneroLikeSettlementThresholdChoice.AtLeastOne,
|
||||
10 => MoneroLikeSettlementThresholdChoice.AtLeastTen,
|
||||
_ => MoneroLikeSettlementThresholdChoice.Custom
|
||||
};
|
||||
}
|
||||
|
||||
return new MoneroLikePaymentMethodViewModel()
|
||||
{
|
||||
WalletFileFound = System.IO.File.Exists(fileAddress),
|
||||
@ -124,9 +129,11 @@ namespace BTCPayServer.Services.Altcoins.Monero.UI
|
||||
Accounts = accounts == null ? null : new SelectList(accounts, nameof(SelectListItem.Value),
|
||||
nameof(SelectListItem.Text)),
|
||||
SettlementConfirmationThresholdChoice = settlementThresholdChoice,
|
||||
CustomSettlementConfirmationThreshold = settlementThresholdChoice is MoneroLikeSettlementThresholdChoice.Custom
|
||||
? settings.InvoiceSettledConfirmationThreshold
|
||||
: null
|
||||
CustomSettlementConfirmationThreshold =
|
||||
settings != null &&
|
||||
settlementThresholdChoice is MoneroLikeSettlementThresholdChoice.Custom
|
||||
? settings.InvoiceSettledConfirmationThreshold
|
||||
: null
|
||||
};
|
||||
}
|
||||
|
||||
@ -242,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."
|
||||
|
||||
});
|
||||
}
|
||||
|
133
BTCPayServer/Services/Reporting/RefundsReportProvider.cs
Normal file
133
BTCPayServer/Services/Reporting/RefundsReportProvider.cs
Normal file
@ -0,0 +1,133 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using Dapper;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace BTCPayServer.Services.Reporting
|
||||
{
|
||||
public class RefundsReportProvider : ReportProvider
|
||||
{
|
||||
private readonly BTCPayNetworkJsonSerializerSettings _serializerSettings;
|
||||
private readonly DisplayFormatter _displayFormatter;
|
||||
|
||||
private ViewDefinition CreateDefinition()
|
||||
{
|
||||
return new ViewDefinition
|
||||
{
|
||||
Fields = new List<StoreReportResponse.Field>
|
||||
{
|
||||
new("Date", "datetime"),
|
||||
new("InvoiceId", "invoice_id"),
|
||||
new("Currency", "string"),
|
||||
new("Completed", "amount"),
|
||||
new("Awaiting", "amount"),
|
||||
new("Limit", "amount"),
|
||||
new("FullyPaid", "boolean")
|
||||
},
|
||||
Charts =
|
||||
{
|
||||
new ()
|
||||
{
|
||||
Name = "Aggregated amount",
|
||||
Groups = { "Currency" },
|
||||
HasGrandTotal = false,
|
||||
Aggregates = { "Awaiting", "Completed", "Limit" }
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
public override string Name => "Refunds";
|
||||
|
||||
public ApplicationDbContextFactory DbContextFactory { get; }
|
||||
|
||||
public RefundsReportProvider(
|
||||
ApplicationDbContextFactory dbContextFactory,
|
||||
BTCPayNetworkJsonSerializerSettings serializerSettings,
|
||||
DisplayFormatter displayFormatter)
|
||||
{
|
||||
DbContextFactory = dbContextFactory;
|
||||
_serializerSettings = serializerSettings;
|
||||
_displayFormatter = displayFormatter;
|
||||
}
|
||||
record RefundRow(DateTimeOffset Created, string InvoiceId, string PullPaymentId, string Currency, decimal Limit)
|
||||
{
|
||||
public decimal Completed { get; set; }
|
||||
public decimal Awaiting { get; set; }
|
||||
}
|
||||
public override async Task Query(QueryContext queryContext, CancellationToken cancellation)
|
||||
{
|
||||
queryContext.ViewDefinition = CreateDefinition();
|
||||
RefundRow? currentRow = null;
|
||||
await using var ctx = DbContextFactory.CreateContext();
|
||||
var conn = ctx.Database.GetDbConnection();
|
||||
var rows = await conn.QueryAsync(
|
||||
"""
|
||||
SELECT i."Created", i."Id" AS "InvoiceId", p."State", p."PaymentMethodId", pp."Id" AS "PullPaymentId", pp."Blob" AS "ppBlob", p."Blob" AS "pBlob" FROM "Invoices" i
|
||||
JOIN "Refunds" r ON r."InvoiceDataId"= i."Id"
|
||||
JOIN "PullPayments" pp ON r."PullPaymentDataId"=pp."Id"
|
||||
LEFT JOIN "Payouts" p ON p."PullPaymentDataId"=pp."Id"
|
||||
WHERE i."StoreDataId" = @storeId
|
||||
AND i."Created" >= @start AND i."Created" <= @end
|
||||
AND pp."Archived" IS FALSE
|
||||
ORDER BY i."Created", pp."Id"
|
||||
""", new { start = queryContext.From, end = queryContext.To, storeId = queryContext.StoreId });
|
||||
foreach (var r in rows)
|
||||
{
|
||||
PullPaymentBlob ppBlob = GetPullPaymentBlob(r);
|
||||
PayoutBlob? pBlob = GetPayoutBlob(r);
|
||||
|
||||
if ((string)r.PullPaymentId != currentRow?.PullPaymentId)
|
||||
{
|
||||
AddRow(queryContext, currentRow);
|
||||
currentRow = new(r.Created, r.InvoiceId, r.PullPaymentId, ppBlob.Currency, ppBlob.Limit);
|
||||
}
|
||||
if (pBlob is null)
|
||||
continue;
|
||||
var state = Enum.Parse<PayoutState>((string)r.State);
|
||||
if (state == PayoutState.Cancelled)
|
||||
continue;
|
||||
if (state is PayoutState.Completed)
|
||||
currentRow.Completed += pBlob.Amount;
|
||||
else
|
||||
currentRow.Awaiting += pBlob.Amount;
|
||||
}
|
||||
AddRow(queryContext, currentRow);
|
||||
}
|
||||
|
||||
private PayoutBlob? GetPayoutBlob(dynamic r)
|
||||
{
|
||||
if (r.pBlob is null)
|
||||
return null;
|
||||
Data.PayoutData p = new Data.PayoutData();
|
||||
p.PaymentMethodId = r.PaymentMethodId;
|
||||
p.Blob = (string)r.pBlob;
|
||||
return p.GetBlob(_serializerSettings);
|
||||
}
|
||||
|
||||
private static PullPaymentBlob GetPullPaymentBlob(dynamic r)
|
||||
{
|
||||
Data.PullPaymentData pp = new Data.PullPaymentData();
|
||||
pp.Blob = (string)r.ppBlob;
|
||||
return pp.GetBlob();
|
||||
}
|
||||
|
||||
private void AddRow(QueryContext queryContext, RefundRow? currentRow)
|
||||
{
|
||||
if (currentRow is null)
|
||||
return;
|
||||
var data = queryContext.AddData();
|
||||
data.Add(currentRow.Created);
|
||||
data.Add(currentRow.InvoiceId);
|
||||
data.Add(currentRow.Currency);
|
||||
data.Add(_displayFormatter.ToFormattedAmount(currentRow.Completed, currentRow.Currency));
|
||||
data.Add(_displayFormatter.ToFormattedAmount(currentRow.Awaiting, currentRow.Currency));
|
||||
data.Add(_displayFormatter.ToFormattedAmount(currentRow.Limit, currentRow.Currency));
|
||||
data.Add(currentRow.Limit <= currentRow.Completed);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -91,7 +91,7 @@ namespace BTCPayServer.Services.Wallets.Export
|
||||
{
|
||||
using StringWriter writer = new();
|
||||
using var csvWriter = new CsvHelper.CsvWriter(writer, new CsvConfiguration(CultureInfo.InvariantCulture), true);
|
||||
csvWriter.Configuration.RegisterClassMap<ExportTransactionMap>();
|
||||
csvWriter.Context.RegisterClassMap<ExportTransactionMap>();
|
||||
csvWriter.WriteHeader<ExportTransaction>();
|
||||
csvWriter.NextRecord();
|
||||
csvWriter.WriteRecords(invoices);
|
||||
@ -105,7 +105,7 @@ namespace BTCPayServer.Services.Wallets.Export
|
||||
public ExportTransactionMap()
|
||||
{
|
||||
AutoMap(CultureInfo.InvariantCulture);
|
||||
Map(m => m.Labels).ConvertUsing(row => row.Labels == null ? string.Empty : string.Join(", ", row.Labels));
|
||||
Map(m => m.Labels).Convert(row => row.Value.Labels == null ? string.Empty : string.Join(", ", row.Value.Labels));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -326,7 +326,7 @@
|
||||
<div class="accordion-body">
|
||||
<div class="form-group">
|
||||
<label asp-for="CustomCSSLink" class="form-label"></label>
|
||||
<a href="https://docs.btcpayserver.org/Development/Theme/#2-bootstrap-themes" target="_blank" rel="noreferrer noopener" title="More information...">
|
||||
<a href="https://docs.btcpayserver.org/Development/Theme/" target="_blank" rel="noreferrer noopener" title="More information...">
|
||||
<vc:icon symbol="info" />
|
||||
</a>
|
||||
<input asp-for="CustomCSSLink" class="form-control" />
|
||||
|
@ -1,6 +1,7 @@
|
||||
@using BTCPayServer.Client
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@using BTCPayServer.Abstractions.TagHelpers
|
||||
@using BTCPayServer.Models.ServerViewModels
|
||||
@model BTCPayServer.Models.EmailsViewModel
|
||||
|
||||
<div class="row">
|
||||
@ -44,7 +45,7 @@
|
||||
<div class="form-text">For many email providers (like Gmail) your login is your email address.</div>
|
||||
<span asp-validation-for="Settings.Login" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group" permission="@Policies.CanModifyStoreSettings">
|
||||
<div class="form-group" permission="@(Model is ServerEmailsViewModel ? Policies.CanModifyServerSettings : Policies.CanModifyStoreSettings)">
|
||||
@if (!Model.PasswordSet)
|
||||
{
|
||||
<label asp-for="Settings.Password" class="form-label"></label>
|
||||
@ -81,7 +82,6 @@
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
|
||||
delegate('click', '#quick-fill .dropdown-menu a', function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
|
@ -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">
|
||||
|
@ -304,7 +304,7 @@
|
||||
<div class="accordion-body">
|
||||
<div class="form-group">
|
||||
<label asp-for="CustomCSSLink" class="form-label"></label>
|
||||
<a href="https://docs.btcpayserver.org/Development/Theme/#2-bootstrap-themes" target="_blank" rel="noreferrer noopener" title="More information...">
|
||||
<a href="https://docs.btcpayserver.org/Development/Theme/" target="_blank" rel="noreferrer noopener" title="More information...">
|
||||
<vc:icon symbol="info" />
|
||||
</a>
|
||||
<input asp-for="CustomCSSLink" class="form-control" />
|
||||
|
@ -1,3 +1,4 @@
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@model (Dictionary<string, object> Items, int Level)
|
||||
|
||||
@functions {
|
||||
@ -10,14 +11,20 @@
|
||||
|
||||
@if (Model.Items.Any())
|
||||
{
|
||||
var hasCart = Model.Items.ContainsKey("Cart");
|
||||
@* Use titlecase and lowercase versions for backwards-compatibility *@
|
||||
string[] cartKeys = ["cart", "subtotal", "discount", "tip", "total"];
|
||||
<table class="table my-0" v-pre>
|
||||
@if (hasCart || (Model.Items.ContainsKey("Subtotal") && Model.Items.ContainsKey("Total")))
|
||||
@if (Model.Items.Keys.Any(key => cartKeys.Contains(key.ToLowerInvariant())))
|
||||
{
|
||||
@if (hasCart)
|
||||
_ = Model.Items.TryGetValue("cart", out var cart) || Model.Items.TryGetValue("Cart", out cart);
|
||||
var hasTotal = Model.Items.TryGetValue("total", out var total) || Model.Items.TryGetValue("Total", out total);
|
||||
var hasSubtotal = Model.Items.TryGetValue("subtotal", out var subtotal) || Model.Items.TryGetValue("subTotal", out subtotal) || Model.Items.TryGetValue("Subtotal", out subtotal);
|
||||
var hasDiscount = Model.Items.TryGetValue("discount", out var discount) || Model.Items.TryGetValue("Discount", out discount);
|
||||
var hasTip = Model.Items.TryGetValue("tip", out var tip) || Model.Items.TryGetValue("Tip", out tip);
|
||||
if (cart is Dictionary<string, object> { Keys.Count: > 0 } cartDict)
|
||||
{
|
||||
<tbody>
|
||||
@foreach (var (key, value) in (Dictionary<string, object>)Model.Items["Cart"])
|
||||
@foreach (var (key, value) in cartDict)
|
||||
{
|
||||
<tr>
|
||||
<th>@key</th>
|
||||
@ -26,35 +33,46 @@
|
||||
}
|
||||
</tbody>
|
||||
}
|
||||
<tfoot style="border-top-width:@(hasCart ? "3px" : "0")">
|
||||
@if (Model.Items.ContainsKey("Subtotal"))
|
||||
else if (cart is ICollection<object> { Count: > 0 } cartCollection)
|
||||
{
|
||||
<tbody>
|
||||
@foreach (var value in cartCollection)
|
||||
{
|
||||
<tr>
|
||||
<th>Subtotal</th>
|
||||
<td class="text-end">@Model.Items["Subtotal"]</td>
|
||||
</tr>
|
||||
}
|
||||
@if (Model.Items.ContainsKey("Discount"))
|
||||
{
|
||||
<tr>
|
||||
<th>Discount</th>
|
||||
<td class="text-end">@Model.Items["Discount"]</td>
|
||||
</tr>
|
||||
}
|
||||
@if (Model.Items.ContainsKey("Tip"))
|
||||
{
|
||||
<tr>
|
||||
<th>Tip</th>
|
||||
<td class="text-end">@Model.Items["Tip"]</td>
|
||||
</tr>
|
||||
}
|
||||
@if (Model.Items.ContainsKey("Total"))
|
||||
{
|
||||
<tr style="border-top-width:3px">
|
||||
<th>Total</th>
|
||||
<td class="text-end">@Model.Items["Total"]</td>
|
||||
<td>@value</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
}
|
||||
<tfoot style="border-top-width:0">
|
||||
@if (hasSubtotal && (hasDiscount || hasTip))
|
||||
{
|
||||
<tr style="border-top-width:3px">
|
||||
<th>Subtotal</th>
|
||||
<td class="text-end">@subtotal</td>
|
||||
</tr>
|
||||
}
|
||||
@if (hasDiscount)
|
||||
{
|
||||
<tr>
|
||||
<th>Discount</th>
|
||||
<td class="text-end">@discount</td>
|
||||
</tr>
|
||||
}
|
||||
@if (hasTip)
|
||||
{
|
||||
<tr>
|
||||
<th>Tip</th>
|
||||
<td class="text-end">@tip</td>
|
||||
</tr>
|
||||
}
|
||||
@if (hasTotal)
|
||||
{
|
||||
<tr style="border-top-width:3px">
|
||||
<th>Total</th>
|
||||
<td class="text-end">@total</td>
|
||||
</tr>
|
||||
}
|
||||
</tfoot>
|
||||
}
|
||||
else
|
||||
|
File diff suppressed because one or more lines are too long
@ -18,7 +18,10 @@
|
||||
@RenderBody()
|
||||
</div>
|
||||
</section>
|
||||
<partial name="_Footer"/>
|
||||
@if (ViewData["ShowFooter"] is not false)
|
||||
{
|
||||
<partial name="_Footer"/>
|
||||
}
|
||||
<partial name="LayoutFoot" />
|
||||
@await RenderSectionAsync("PageFootContent", false)
|
||||
</body>
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -431,7 +431,7 @@
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
@if (Model.ReceiptData != null && Model.ReceiptData.Any())
|
||||
@if (Model.ReceiptData?.Any() is true)
|
||||
{
|
||||
<div>
|
||||
<h3 class="mb-3">
|
||||
@ -443,7 +443,7 @@
|
||||
<partial name="PosData" model="(Model.ReceiptData, 1)" />
|
||||
</div>
|
||||
}
|
||||
@if (Model.AdditionalData != null && Model.AdditionalData.Any())
|
||||
@if (Model.AdditionalData?.Any() is true)
|
||||
{
|
||||
<div>
|
||||
<h3 class="mb-3">
|
||||
|
@ -35,8 +35,8 @@
|
||||
#InvoiceSummary { gap: var(--btcpay-space-l); }
|
||||
#PaymentDetails table tbody tr:first-child td { padding-top: 1rem; }
|
||||
#PaymentDetails table tbody:not(:last-child) tr:last-child > th,td { padding-bottom: 1rem; }
|
||||
#posData td > table:last-child { margin-bottom: 0 !important; }
|
||||
#posData table > tbody > tr:first-child > td > h4 { margin-top: 0 !important; }
|
||||
#AdditionalData td > table:last-child, #CartData td > table:last-child { margin-bottom: 0 !important; }
|
||||
#AdditionalData table > tbody > tr:first-child > td > h4, #CartData table > tbody > tr:first-child > td > h4 { margin-top: 0 !important; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="min-vh-100">
|
||||
@ -62,7 +62,7 @@
|
||||
{
|
||||
if (Model.ReceiptOptions.ShowQR is true)
|
||||
{
|
||||
<vc:qr-code data="@Context.Request.GetCurrentUrl()"></vc:qr-code>
|
||||
<vc:qr-code data="@Context.Request.GetCurrentUrl()" />
|
||||
}
|
||||
<div class="d-flex gap-4 mb-0 flex-fill">
|
||||
<dl class="d-flex flex-column gap-4 mb-0 flex-fill">
|
||||
@ -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>
|
||||
@ -102,6 +102,15 @@
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
if (Model.CartData?.Any() is true)
|
||||
{
|
||||
<div id="CartData" class="tile">
|
||||
<h2 class="h4 mb-3">Cart</h2>
|
||||
<div class="table-responsive my-0">
|
||||
<partial name="PosData" model="(Model.CartData, 1)" />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
if (Model.Payments?.Any() is true)
|
||||
{
|
||||
<div id="PaymentDetails" class="tile">
|
||||
@ -164,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>
|
||||
|
@ -1,4 +1,17 @@
|
||||
@model BTCPayServer.Models.InvoicingModels.InvoiceReceiptViewModel
|
||||
|
||||
@functions {
|
||||
public bool IsManualEntryCart(Dictionary<string, object> additionalData)
|
||||
{
|
||||
_ = additionalData.TryGetValue("cart", out var data) || additionalData.TryGetValue("Cart", out data);
|
||||
if (data is Dictionary<string, object> cart)
|
||||
{
|
||||
return cart.Count == 1 && cart.ContainsKey("Manual entry 1");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@using BTCPayServer.Client.Models
|
||||
@using BTCPayServer.Components.QRCode
|
||||
@using BTCPayServer.Services
|
||||
@ -90,6 +103,7 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
var hasCart = Model.CartData?.Any() is true;
|
||||
<div id="PaymentDetails">
|
||||
<div class="my-2 text-center small">
|
||||
@if (!string.IsNullOrEmpty(Model.OrderId))
|
||||
@ -99,53 +113,92 @@
|
||||
@Model.Timestamp.ToBrowserDate()
|
||||
</div>
|
||||
<table class="table table-borderless table-sm small my-0">
|
||||
<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>
|
||||
<tr>
|
||||
<td colspan="2"><hr class="w-100 my-0"/></td>
|
||||
</tr>
|
||||
@if (Model.AdditionalData?.Any() is true &&
|
||||
(Model.AdditionalData.ContainsKey("Cart") || Model.AdditionalData.ContainsKey("Discount") || Model.AdditionalData.ContainsKey("Tip")))
|
||||
@if (Model.AdditionalData?.Any() is true)
|
||||
{
|
||||
@if (Model.AdditionalData.ContainsKey("Cart"))
|
||||
@foreach (var (key, value) in Model.AdditionalData)
|
||||
{
|
||||
@foreach (var (key, value) in (Dictionary<string, object>)Model.AdditionalData["Cart"])
|
||||
{
|
||||
<tr>
|
||||
<td class="text-secondary">@key</td>
|
||||
<td class="text-end">@value</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
@if (Model.AdditionalData.ContainsKey("Subtotal"))
|
||||
{
|
||||
<tr>
|
||||
<td class="text-secondary">Subtotal</td>
|
||||
<td class="text-end">@Model.AdditionalData["Subtotal"]</td>
|
||||
</tr>
|
||||
}
|
||||
@if (Model.AdditionalData.ContainsKey("Discount"))
|
||||
{
|
||||
<tr>
|
||||
<td class="text-secondary">Discount</td>
|
||||
<td class="text-end">@Model.AdditionalData["Discount"]</td>
|
||||
</tr>
|
||||
}
|
||||
@if (Model.AdditionalData.ContainsKey("Tip"))
|
||||
{
|
||||
<tr>
|
||||
<td class="text-secondary">Tip</td>
|
||||
<td class="text-end">@Model.AdditionalData["Tip"]</td>
|
||||
<tr class="additional-data">
|
||||
<td class="text-secondary">@key</td>
|
||||
<td class="text-end">@value</td>
|
||||
</tr>
|
||||
}
|
||||
<tr>
|
||||
<td colspan="2"><hr class="w-100 my-0"/></td>
|
||||
</tr>
|
||||
}
|
||||
@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)
|
||||
{
|
||||
@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>
|
||||
}
|
||||
}
|
||||
else if (cart is ICollection<object> { Count: > 0 } cartCollection)
|
||||
{
|
||||
@foreach (var value in cartCollection)
|
||||
{
|
||||
<tr>
|
||||
<td class="val text-end">@value</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
if (hasSubtotal && (hasDiscount || hasTip))
|
||||
{
|
||||
<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>
|
||||
}
|
||||
if (hasDiscount)
|
||||
{
|
||||
<tr class="sums-data">
|
||||
<td class="key text-secondary">Discount</td>
|
||||
<td class="val 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>
|
||||
}
|
||||
if (hasTotal)
|
||||
{
|
||||
<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>
|
||||
}
|
||||
}
|
||||
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>
|
||||
}
|
||||
@if (Model.Payments?.Any() is true)
|
||||
{
|
||||
<tr>
|
||||
<td colspan="2"><hr class="w-100 my-0"/></td>
|
||||
</tr>
|
||||
@for (var i = 0; i < Model.Payments.Count; i++)
|
||||
{
|
||||
var payment = Model.Payments[i];
|
||||
@ -154,33 +207,33 @@
|
||||
<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)
|
||||
{
|
||||
<span>
|
||||
<span>@payment.Destination[..30]</span>
|
||||
<span>@payment.Destination[..19]</span>
|
||||
<span>...</span>
|
||||
<span>@payment.Destination.Substring(payment.Destination.Length - 30, 30)</span>
|
||||
<span>@payment.Destination.Substring(payment.Destination.Length - 20, 20)</span>
|
||||
</span>
|
||||
}
|
||||
else
|
||||
@ -192,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>
|
||||
@ -216,7 +269,9 @@
|
||||
<hr class="w-100 my-0 bg-none"/>
|
||||
</center>
|
||||
</body>
|
||||
<script src="~/main/utils.js" asp-append-version="true"></script>
|
||||
<script>
|
||||
formatDateTimes();
|
||||
window.print();
|
||||
</script>
|
||||
</html>
|
||||
|
@ -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";
|
||||
}
|
||||
|
@ -144,7 +144,7 @@
|
||||
<div class="accordion-body">
|
||||
<div class="form-group">
|
||||
<label asp-for="CustomCSSLink" class="form-label"></label>
|
||||
<a href="https://docs.btcpayserver.org/Development/Theme/#2-bootstrap-themes" target="_blank" rel="noreferrer noopener" title="More information...">
|
||||
<a href="https://docs.btcpayserver.org/Development/Theme/" target="_blank" rel="noreferrer noopener" title="More information...">
|
||||
<vc:icon symbol="info" />
|
||||
</a>
|
||||
<input asp-for="CustomCSSLink" class="form-control" />
|
||||
|
@ -118,7 +118,10 @@
|
||||
</p>
|
||||
<dl class="mt-n1 mb-4" v-if="srvModel.amountCollected > 0 && srvModel.amountDue > 0">
|
||||
<div class="progress bg-light d-flex mb-3 d-print-none" style="height:5px">
|
||||
<div class="progress-bar bg-primary" role="progressbar" style="width:@(Model.AmountCollected/Model.Amount*100)%" v-bind:style="{ width: (srvModel.amountCollected/srvModel.amount*100) + '%' }"></div>
|
||||
@{
|
||||
var prcnt = Model.Amount == 0? 100: Model.AmountCollected / Model.Amount * 100;
|
||||
}
|
||||
<div class="progress-bar bg-primary" role="progressbar" style="width:@prcnt%" v-bind:style="{ width: (srvModel.amountCollected/srvModel.amount*100) + '%' }"></div>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap gap-3 align-items-center justify-content-between">
|
||||
<div class="d-flex d-print-inline-block flex-column gap-1">
|
||||
|
@ -74,7 +74,7 @@
|
||||
<div class="accordion-body">
|
||||
<div class="form-group">
|
||||
<label asp-for="CustomCSSLink" class="form-label"></label>
|
||||
<a href="https://docs.btcpayserver.org/Development/Theme/#2-bootstrap-themes" target="_blank" rel="noreferrer noopener">
|
||||
<a href="https://docs.btcpayserver.org/Development/Theme/" target="_blank" rel="noreferrer noopener">
|
||||
<vc:icon symbol="info" />
|
||||
</a>
|
||||
<input asp-for="CustomCSSLink" class="form-control" />
|
||||
|
@ -58,7 +58,7 @@
|
||||
<select class="form-select w-auto" asp-for="SelectedPaymentMethod" asp-items="Model.PaymentMethods.Select(id => new SelectListItem(id.ToPrettyString(), id.ToString()))"></select>
|
||||
}
|
||||
<button type="button" class="btn btn-secondary only-for-js" data-bs-toggle="modal" data-bs-target="#scanModal" title="Scan destination with camera" id="scandestination-button">
|
||||
<i class="fa fa-camera"></i>
|
||||
<vc:icon symbol="scan-qr"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -202,7 +202,7 @@
|
||||
</p>
|
||||
@if (Model.LnurlEndpoint is not null)
|
||||
{
|
||||
<p id="BoltcardActions" style="visibility:hidden">
|
||||
<p id="BoltcardActions">
|
||||
<a id="SetupBoltcard" asp-action="SetupBoltcard" asp-controller="UIPullPayment" asp-route-pullPaymentId="@Model.Id" asp-route-command="configure-boltcard">
|
||||
Setup Boltcard
|
||||
</a>
|
||||
@ -226,14 +226,13 @@
|
||||
<script src="~/vendor/ur-registry/urlib.min.js" asp-append-version="true"></script>
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const isAndroid = /(android)/i.test(navigator.userAgent);
|
||||
if (isAndroid) {
|
||||
var isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
||||
if (isMobile) {
|
||||
document.getElementById("SetupBoltcard").setAttribute('target', '_blank');
|
||||
document.getElementById("SetupBoltcard").setAttribute('href', @Safe.Json(@Model.SetupDeepLink));
|
||||
document.getElementById("SetupBoltcard").setAttribute('href', @Safe.Json(Model.SetupDeepLink));
|
||||
document.getElementById("ResetBoltcard").setAttribute('target', '_blank');
|
||||
document.getElementById("ResetBoltcard").setAttribute('href', @Safe.Json(@Model.ResetDeepLink));
|
||||
document.getElementById("ResetBoltcard").setAttribute('href', @Safe.Json(Model.ResetDeepLink));
|
||||
}
|
||||
document.getElementById("BoltcardActions").style.visibility = "visible";
|
||||
|
||||
window.qrApp = initQRShow({});
|
||||
delegate('click', 'button[page-qr]', event => {
|
||||
|
@ -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>
|
||||
}
|
||||
|
||||
@ -32,7 +33,7 @@
|
||||
</a>
|
||||
</h2>
|
||||
<div class="d-flex flex-wrap gap-3">
|
||||
<a cheat-mode="true" class="btn btn-outline-info text-nowrap" asp-action="StoreReports" asp-route-fakeData="true" asp-route-viewName="@Model.Request?.ViewName">Create fake date</a>
|
||||
<a cheat-mode="true" class="btn btn-outline-info text-nowrap" asp-action="StoreReports" asp-route-fakeData="true" asp-route-viewName="@Model.Request?.ViewName">Create fake data</a>
|
||||
<button id="exportCSV" class="btn btn-primary text-nowrap" type="button">Export</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -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>
|
||||
|
@ -69,7 +69,7 @@
|
||||
<div>
|
||||
<label asp-for="CustomTheme" class="form-check-label"></label>
|
||||
<div class="text-muted">
|
||||
<a href="https://docs.btcpayserver.org/Development/Theme/#1-custom-themes" target="_blank" rel="noreferrer noopener">Adjust the design</a>
|
||||
<a href="https://docs.btcpayserver.org/Development/Theme/" target="_blank" rel="noreferrer noopener">Adjust the design</a>
|
||||
of your BTCPay Server instance to your needs.
|
||||
</div>
|
||||
</div>
|
||||
|
@ -84,7 +84,7 @@
|
||||
@plugin
|
||||
@if (version != null)
|
||||
{
|
||||
<span>({version})</span>
|
||||
<span>(@version)</span>
|
||||
}
|
||||
</span>
|
||||
<form asp-action="UnInstallPlugin" asp-route-plugin="@plugin">
|
||||
|
@ -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 };
|
||||
|
@ -110,7 +110,7 @@
|
||||
<div class="accordion-body">
|
||||
<div class="form-group">
|
||||
<label asp-for="CustomCSSLink" class="form-label"></label>
|
||||
<a href="https://docs.btcpayserver.org/Development/Theme/#2-bootstrap-themes" target="_blank" rel="noreferrer noopener" title="More information...">
|
||||
<a href="https://docs.btcpayserver.org/Development/Theme/" target="_blank" rel="noreferrer noopener" title="More information...">
|
||||
<vc:icon symbol="info" />
|
||||
</a>
|
||||
<input asp-for="CustomCSSLink" class="form-control" />
|
||||
|
@ -7,8 +7,6 @@
|
||||
@model BTCPayServer.Models.WalletViewModels.PayoutsModel
|
||||
|
||||
@inject IEnumerable<IPayoutHandler> PayoutHandlers;
|
||||
@inject PayoutProcessorService _payoutProcessorService;
|
||||
@inject IEnumerable<IPayoutProcessorFactory> _payoutProcessorFactories;
|
||||
@{
|
||||
var storeId = Context.GetRouteValue("storeId") as string;
|
||||
ViewData.SetActivePage(StoreNavPages.Payouts, $"Payouts{(string.IsNullOrEmpty(Model.PullPaymentName) ? string.Empty : " for pull payment " + Model.PullPaymentName)}", Context.GetStoreData().Id);
|
||||
@ -24,15 +22,16 @@
|
||||
return;
|
||||
stateActions.AddRange(payoutHandler.GetPayoutSpecificActions().Where(pair => pair.Key == Model.PayoutState).SelectMany(pair => pair.Value));
|
||||
}
|
||||
|
||||
switch (Model.PayoutState)
|
||||
{
|
||||
case PayoutState.AwaitingApproval:
|
||||
if (!Model.HasPayoutProcessor) stateActions.Add(("approve-pay", "Approve & Send"));
|
||||
stateActions.Add(("approve", "Approve"));
|
||||
stateActions.Add(("approve-pay", "Approve & Send"));
|
||||
stateActions.Add(("cancel", "Cancel"));
|
||||
break;
|
||||
case PayoutState.AwaitingPayment:
|
||||
stateActions.Add(("pay", "Send"));
|
||||
if (!Model.HasPayoutProcessor) stateActions.Add(("pay", "Send"));
|
||||
stateActions.Add(("cancel", "Cancel"));
|
||||
stateActions.Add(("mark-paid", "Mark as already paid"));
|
||||
break;
|
||||
@ -87,11 +86,7 @@
|
||||
|
||||
<partial name="_StatusMessage" />
|
||||
|
||||
@if (_payoutProcessorFactories.Any(factory => factory.GetSupportedPaymentMethods().Contains(paymentMethodId)) && !(await _payoutProcessorService.GetProcessors(new PayoutProcessorService.PayoutProcessorQuery()
|
||||
{
|
||||
Stores = new[] { storeId },
|
||||
PaymentMethods = new[] { Model.PaymentMethodId }
|
||||
})).Any())
|
||||
@if (!Model.HasPayoutProcessor)
|
||||
{
|
||||
<div class="alert alert-info mb-5" role="alert" permission="@Policies.CanModifyStoreSettings">
|
||||
<strong>Pro tip:</strong> There are supported but unconfigured Payout Processors for this payout payment method.<br/>
|
||||
|
@ -205,7 +205,7 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="CustomLogo" class="form-label"></label>
|
||||
<a href="https://docs.btcpayserver.org/Development/Theme/#checkout-page-themes" target="_blank" rel="noreferrer noopener">
|
||||
<a href="https://docs.btcpayserver.org/Development/Theme/" target="_blank" rel="noreferrer noopener">
|
||||
<vc:icon symbol="info" />
|
||||
</a>
|
||||
<input asp-for="CustomLogo" class="form-control" />
|
||||
@ -213,7 +213,7 @@
|
||||
</div>
|
||||
<div class="form-group mb-0">
|
||||
<label asp-for="CustomCSS" class="form-label"></label>
|
||||
<a href="https://docs.btcpayserver.org/Development/Theme/#checkout-page-themes" target="_blank" rel="noreferrer noopener">
|
||||
<a href="https://docs.btcpayserver.org/Development/Theme/" target="_blank" rel="noreferrer noopener">
|
||||
<vc:icon symbol="info" />
|
||||
</a>
|
||||
<input asp-for="CustomCSS" class="form-control" />
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -41,6 +41,7 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<input type="hidden" name="UseCustomSMTP" value="true" />
|
||||
<partial name="EmailsBody" model="Model" />
|
||||
}
|
||||
|
||||
|
@ -1 +1,12 @@
|
||||
<svg width="150" height="100" style="clip-rule:evenodd;fill-rule:evenodd;image-rendering:optimizeQuality;shape-rendering:geometricPrecision;text-rendering:geometricPrecision" version="1.1" id="svg587" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><style id="style324">.st2{fill:#ffc214}.st3{fill:#f9f185}.st0{fill:#222221}.st1{fill:#272425}</style><g id="g931" transform="matrix(.375 0 0 .375 -306.863 -123.51)"><path fill-rule="evenodd" clip-rule="evenodd" d="M911.118 436.635c-7.865-3.02-11.793-11.842-8.774-19.707s11.842-11.793 19.707-8.774l56.773 21.793c.062.026.126.05.19.074l28.48 10.932c7.864 3.02 16.689-.909 19.706-8.774 3.02-7.865-.908-16.688-8.774-19.707v-.003l-28.48-10.932c-7.865-3.02-11.793-11.842-8.774-19.707 3.02-7.865 11.842-11.793 19.707-8.774l83.768 32.155c.2.077.399.158.594.242a84 84 0 0 1 1.08.406c39.325 15.095 58.967 59.21 43.87 98.535-15.095 39.324-59.21 58.965-98.534 43.87a78.402 78.402 0 0 1-2.249-.903c-.36-.11-.72-.232-1.076-.37l-82.117-31.521c-7.865-3.02-11.793-11.842-8.774-19.707s11.842-11.793 19.707-8.774l28.477 10.931-.002-.007.006.002c7.865 3.02 16.688-.909 19.706-8.774 3.02-7.865-.908-16.688-8.773-19.707l-12.817-4.92v-.001z" fill="currentColor" id="path1" style="clip-rule:evenodd;fill:#000;fill-rule:evenodd;stroke-width:1.52532;image-rendering:optimizeQuality;shape-rendering:geometricPrecision;text-rendering:geometricPrecision"/></g></svg>
|
||||
<svg width="150" height="100" style="clip-rule:evenodd;fill-rule:evenodd;image-rendering:optimizeQuality;shape-rendering:geometricPrecision;text-rendering:geometricPrecision" version="1.1" id="svg587" xml:space="preserve" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="100%" height="100%" fill="#ffffff"/>
|
||||
<style id="style324">
|
||||
.st2{fill:#ffc214}
|
||||
.st3{fill:#f9f185}
|
||||
.st0{fill:#222221}
|
||||
.st1{fill:#272425}
|
||||
</style>
|
||||
<g id="g931" transform="matrix(.375 0 0 .375 -306.863 -123.51)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M911.118 436.635c-7.865-3.02-11.793-11.842-8.774-19.707s11.842-11.793 19.707-8.774l56.773 21.793c.062.026.126.05.19.074l28.48 10.932c7.864 3.02 16.689-.909 19.706-8.774 3.02-7.865-.908-16.688-8.774-19.707v-.003l-28.48-10.932c-7.865-3.02-11.793-11.842-8.774-19.707 3.02-7.865 11.842-11.793 19.707-8.774l83.768 32.155c.2.077.399.158.594.242a84 84 0 0 1 1.08.406c39.325 15.095 58.967 59.21 43.87 98.535-15.095 39.324-59.21 58.965-98.534 43.87a78.402 78.402 0 0 1-2.249-.903c-.36-.11-.72-.232-1.076-.37l-82.117-31.521c-7.865-3.02-11.793-11.842-8.774-19.707s11.842-11.793 19.707-8.774l28.477 10.931-.002-.007.006.002c7.865 3.02 16.688-.909 19.706-8.774 3.02-7.865-.908-16.688-8.773-19.707l-12.817-4.92v-.001z" fill="currentColor" id="path1" style="clip-rule:evenodd;fill:#000;fill-rule:evenodd;stroke-width:1.52532;image-rendering:optimizeQuality;shape-rendering:geometricPrecision;text-rendering:geometricPrecision"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
3
BTCPayServer/wwwroot/img/readme/supporter_tether.svg
Normal file
3
BTCPayServer/wwwroot/img/readme/supporter_tether.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="150" height="100" viewBox="0 0 150 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M24.4825 0.862305H88.0496C89.5663 0.862305 90.9675 1.64827 91.7239 2.92338L110.244 34.1419C111.204 35.7609 110.919 37.8043 109.549 39.1171L58.5729 87.9703C56.9216 89.5528 54.2652 89.5528 52.6139 87.9703L1.70699 39.1831C0.305262 37.8398 0.0427812 35.7367 1.07354 34.1077L20.8696 2.82322C21.6406 1.60483 23.0087 0.862305 24.4825 0.862305ZM79.8419 14.8003V23.5597H61.7343V29.6329C74.4518 30.2819 83.9934 32.9475 84.0642 36.1425L84.0638 42.803C83.993 45.998 74.4518 48.6635 61.7343 49.3125V64.2168H49.7105V49.3125C36.9929 48.6635 27.4513 45.998 27.3805 42.803L27.381 36.1425C27.4517 32.9475 36.9929 30.2819 49.7105 29.6329V23.5597H31.6028V14.8003H79.8419ZM55.7224 44.7367C69.2943 44.7367 80.6382 42.4827 83.4143 39.4727C81.0601 36.9202 72.5448 34.9114 61.7343 34.3597V40.7183C59.7966 40.8172 57.7852 40.8693 55.7224 40.8693C53.6595 40.8693 51.6481 40.8172 49.7105 40.7183V34.3597C38.8999 34.9114 30.3846 36.9202 28.0304 39.4727C30.8066 42.4827 42.1504 44.7367 55.7224 44.7367Z" fill="#009393" transform="translate(20, 5)"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
@ -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);
|
||||
|
@ -185,7 +185,7 @@ function downloadCSV() {
|
||||
|
||||
// Convert ISO8601 dates to YYYY-MM-DD HH:mm:ss so the CSV easily integrate with Excel
|
||||
modifyFields(srv.result.fields, data, 'amount', displayValue)
|
||||
modifyFields(srv.result.fields, data, 'datetime', v => v? moment(v).format('YYYY-MM-DD hh:mm:ss'): v);
|
||||
modifyFields(srv.result.fields, data, 'datetime', v => v ? moment(v).format('YYYY-MM-DD HH:mm:ss') : v);
|
||||
const csv = Papa.unparse({ fields: srv.result.fields.map(f => f.name), data });
|
||||
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
||||
saveAs(blob, "export.csv");
|
||||
|
@ -473,7 +473,7 @@ svg.icon-note {
|
||||
font-weight: var(--btcpay-font-weight-bold);
|
||||
}
|
||||
|
||||
.widget .table {
|
||||
.widget *:not([class*='table-responsive']) > .table {
|
||||
margin-left: -.5rem;
|
||||
margin-right: -.5rem;
|
||||
width: calc(100% + 1rem);
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user