Compare commits
4 Commits
v1.13.x
...
v1.13.1-ho
Author | SHA1 | Date | |
---|---|---|---|
425796a386 | |||
e5a445b383 | |||
335d6f69b5 | |||
105ff29451 |
BTCPayServer.Abstractions
BTCPayServer.Client
BTCPayServer.Common
BTCPayServer.Data
BTCPayServer.PluginPacker
BTCPayServer.Rating
BTCPayServer.Tests
BTCPayServer.Tests.csprojFastTests.csGreenfieldAPITests.csSeleniumTester.csSeleniumTests.csThirdPartyTests.csUnitTest1.csUtilitiesTests.csdocker-compose.altcoins.ymldocker-compose.ymlsetup-dev-basics.sh
BTCPayServer
BTCPayServer.csproj
Components
Controllers
GreenField
GreenfieldAppsController.csGreenfieldInvoiceController.csGreenfieldPullPaymentController.csGreenfieldStoreLightningAddressesController.cs
UIBoltcardController.csUIInvoiceController.UI.csUILNURLController.csUIPullPaymentController.csUIReportsController.CheatMode.csUIStoresController.Email.csUIWalletsController.PSBT.csUIWalletsController.csData/Payouts/LightningLike
DerivationSchemeParser.csFilters
HostedServices
Hosting
Models
InvoicingModels
WalletViewModels
Plugins
Altcoins/Monero/RPC/Models
BoltcardBalance
BoltcardFactory
PayButton/Models
PluginBuilderClient.csPluginExceptionHandler.csPluginManager.csPluginService.csPointOfSale/Controllers
Shopify
Properties
SearchString.csServices
Views
Shared
UIInvoice
UIMoneroLikeStore
UIPaymentRequest
UIPullPayment
UIReports
UIServer
UIStorePullPayments
UIStores
wwwroot
Build
Changelog.mdDockerfiledocs
@ -32,9 +32,9 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="HtmlSanitizer" Version="8.0.838" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.6" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.5" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4" />
|
||||
<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="Pomelo.EntityFrameworkCore.MySql" Version="8.0.0-beta.2" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
@ -31,7 +31,7 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BTCPayServer.Lightning.Common" Version="1.5.1" />
|
||||
<PackageReference Include="NBitcoin" Version="7.0.37" />
|
||||
<PackageReference Include="NBitcoin" Version="7.0.34" />
|
||||
<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/{HttpUtility.UrlEncode(pullPaymentId)}/lnurl",
|
||||
$"/api/v1/pull-payments/{pullPaymentId}/lnurl",
|
||||
method: HttpMethod.Get), cancellationToken);
|
||||
return await HandleResponse<PullPaymentLNURL>(response);
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using NBitcoin.JsonConverters;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
@ -59,8 +58,6 @@ namespace BTCPayServer.Client.JsonConverters
|
||||
return null;
|
||||
return TimeSpan.Zero;
|
||||
}
|
||||
if (reader.TokenType == JsonToken.String && TimeSpan.TryParse(reader.Value?.ToString(), CultureInfo.InvariantCulture, out var res))
|
||||
return res;
|
||||
if (reader.TokenType != JsonToken.Integer)
|
||||
throw new JsonObjectException("Invalid timespan, expected integer", reader);
|
||||
return ToTimespan((long)reader.Value);
|
||||
|
@ -1,5 +1,3 @@
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Client.Models;
|
||||
|
||||
public class LightningAddressData
|
||||
@ -8,5 +6,5 @@ public class LightningAddressData
|
||||
public string CurrencyCode { get; set; }
|
||||
public decimal? Min { get; set; }
|
||||
public decimal? Max { get; set; }
|
||||
public JObject InvoiceMetadata { get; set; }
|
||||
|
||||
}
|
||||
|
@ -1,8 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
@ -16,9 +13,7 @@ namespace BTCPayServer.Client.Models
|
||||
public bool? Archived { get; set; }
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
|
||||
public DateTimeOffset Created { get; set; }
|
||||
[JsonExtensionData]
|
||||
public IDictionary<string, JToken> AdditionalData { get; set; } = new Dictionary<string, JToken>();
|
||||
}
|
||||
}
|
||||
|
||||
public class PointOfSaleAppData : AppDataBase
|
||||
{
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="4.3.1" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="4.3.0" />
|
||||
<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.6">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.7" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\BTCPayServer.Abstractions\BTCPayServer.Abstractions.csproj" />
|
||||
|
@ -43,7 +43,7 @@ public class LightningAddressDataBlob
|
||||
public decimal? Max { get; set; }
|
||||
|
||||
public JObject InvoiceMetadata { get; set; }
|
||||
|
||||
public string PullPaymentId { get; set; }
|
||||
[JsonExtensionData] public Dictionary<string, JToken> AdditionalData { get; set; }
|
||||
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
@ -110,22 +109,9 @@ namespace BTCPayServer.PluginPacker
|
||||
|
||||
private static Type[] GetAllExtensionTypesFromAssembly(Assembly assembly)
|
||||
{
|
||||
return GetLoadableTypes(assembly).Where(type =>
|
||||
return assembly.GetTypes().Where(type =>
|
||||
typeof(IBTCPayServerPlugin).IsAssignableFrom(type) &&
|
||||
!type.IsAbstract).ToArray();
|
||||
}
|
||||
static Type[] GetLoadableTypes(Assembly assembly)
|
||||
{
|
||||
if (assembly == null)
|
||||
throw new ArgumentNullException(nameof(assembly));
|
||||
try
|
||||
{
|
||||
return assembly.GetTypes();
|
||||
}
|
||||
catch (ReflectionTypeLoadException e)
|
||||
{
|
||||
return e.Types.Where(t => t != null).ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,9 +4,9 @@
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
<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="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="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="1.0.4" />
|
||||
</ItemGroup>
|
||||
|
@ -93,7 +93,7 @@ namespace BTCPayServer.Services.Rates
|
||||
if (ticker != null)
|
||||
{
|
||||
var pair = GetCurrencyPair(symbol);
|
||||
if (pair is not null && ticker.Bid <= ticker.Ask)
|
||||
if (pair is not null)
|
||||
result.Add(new PairRate(pair, new BidAsk(ticker.Bid, ticker.Ask)));
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<Import Project="../Build/Common.csproj" />
|
||||
<PropertyGroup>
|
||||
<NoWarn>$(NoWarn),xUnit1031</NoWarn>
|
||||
@ -23,8 +23,8 @@
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
||||
<PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.15" />
|
||||
<PackageReference Include="Selenium.Support" Version="4.1.1" />
|
||||
<PackageReference Include="Selenium.WebDriver" Version="4.22.0" />
|
||||
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="125.0.6422.14100" />
|
||||
<PackageReference Include="Selenium.WebDriver" Version="4.1.1" />
|
||||
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="121.0.6167.8500" />
|
||||
<PackageReference Include="xunit" Version="2.6.6" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.6">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
|
@ -47,6 +47,7 @@ using NBXplorer.DerivationStrategy;
|
||||
using NBXplorer.Models;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using OpenQA.Selenium.DevTools.V100.DOMSnapshot;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
using StoreData = BTCPayServer.Data.StoreData;
|
||||
@ -871,13 +872,6 @@ namespace BTCPayServer.Tests
|
||||
Assert.IsType<MultisigDerivationStrategy>(((P2WSHDerivationStrategy)strategyBase).Inner);
|
||||
Assert.Equal(expected, strategyBase.ToString());
|
||||
|
||||
foreach (var space in new[] { "\r\n", " ", "\t" })
|
||||
{
|
||||
var expectedWithNewLines = $"2-of-tpubDDXgATYzdQkHHhZZCMcNJj8BGDENvzMVou5v9NdxiP4rxDLj33nS233dGFW4htpVZSJ6zds9eVqAV9RyRHHiKtwQKX8eR4n4KN3Dwmj7A3h-{space}tpubDC8a54NFtQtMQAZ97VhoU9V6jVTvi9w4Y5SaAXJSBYETKg3AoX5CCKndznhPWxJUBToPCpT44s86QbKdGpKAnSjcMTGW4kE6UQ8vpBjcybW-tpubDChjnP9LXNrJp43biqjY7FH93wgRRNrNxB4Q8pH7PPRy8UPcH2S6V46WGVJ47zVGF7SyBJNCpnaogsFbsybVQckGtVhCkng3EtFn8qmxptS";
|
||||
strategyBase = parser.Parse(expectedWithNewLines);
|
||||
Assert.Equal(expected, strategyBase.ToString());
|
||||
}
|
||||
|
||||
var inner = (MultisigDerivationStrategy)((P2WSHDerivationStrategy)strategyBase).Inner;
|
||||
Assert.False(inner.IsLegacy);
|
||||
Assert.Equal(3, inner.Keys.Count);
|
||||
@ -894,7 +888,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.True(((DirectDerivationStrategy)strategyBase).Segwit);
|
||||
|
||||
// Failure cases
|
||||
Assert.Throws<FormatException>(() => { parser.Parse("xpubZ661MyMwAqRbcGVBsTGeNZN6QGVHmMHLdSA4FteGsRrEriu4pnVZMZWnruFFFXkMnyoBjyHndD3Qwcfz4MPzBUxjSevweNFQx7SAYZATtcDw"); });
|
||||
Assert.Throws<FormatException>(() => { parser.Parse("xpub 661MyMwAqRbcGVBsTGeNZN6QGVHmMHLdSA4FteGsRrEriu4pnVZMZWnruFFFXkMnyoBjyHndD3Qwcfz4MPzBUxjSevweNFQx7SAYZATtcDw"); }); // invalid format because of space
|
||||
Assert.Throws<ParsingException>(() => { parser.ParseOutputDescriptor("invalid"); }); // invalid in general
|
||||
Assert.Throws<ParsingException>(() => { parser.ParseOutputDescriptor("wpkh([8b60afd1/49h/0h/0h]xpub661MyMwAFXkMnyoBjyHndD3QwRbcGVBsTGeNZN6QGVHcfz4MPzBUxjSevweNFQx7SqmMHLdSA4FteGsRrEriu4pnVZMZWnruFFAYZATtcDw/0/*)#9x4vkw48"); }); // invalid checksum
|
||||
}
|
||||
@ -1353,22 +1347,17 @@ bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku
|
||||
|
||||
filter = "status:abed, status:abed2";
|
||||
search = new SearchString(filter);
|
||||
Assert.Null(search.TextSearch);
|
||||
Assert.Null(search.TextFilters);
|
||||
Assert.Equal("", search.TextSearch);
|
||||
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,orderid:MYORDERID,orderid:MYORDERID_2";
|
||||
filter = "StartDate:2019-04-25 01:00 AM, hekki";
|
||||
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";
|
||||
|
@ -1117,8 +1117,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.DoesNotContain(null, card1keys);
|
||||
var card2 = await client.RegisterBoltcard(test4.Id, new RegisterBoltcardRequest()
|
||||
{
|
||||
UID = uid,
|
||||
OnExisting = OnExistingBehavior.UpdateVersion
|
||||
UID = uid
|
||||
});
|
||||
Assert.Equal(1, card2.Version);
|
||||
Assert.StartsWith("lnurlw://", card2.LNURLW);
|
||||
@ -2161,17 +2160,6 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal("BTC", pp.Currency);
|
||||
Assert.True(pp.AutoApproveClaims);
|
||||
Assert.Equal(0.79m, pp.Amount);
|
||||
|
||||
// If an invoice doesn't have payment because it has been marked as paid, we should still be able to refund it.
|
||||
invoice = await client.CreateInvoice(user.StoreId, new CreateInvoiceRequest { Amount = 5000.0m, Currency = "USD" });
|
||||
await client.MarkInvoiceStatus(user.StoreId, invoice.Id, new MarkInvoiceStatusRequest { Status = InvoiceStatus.Settled });
|
||||
var refund = await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest
|
||||
{
|
||||
PaymentMethod = method.PaymentMethod,
|
||||
RefundVariant = RefundVariant.CurrentRate
|
||||
});
|
||||
Assert.Equal(1.0m, refund.Amount);
|
||||
Assert.Equal("BTC", refund.Currency);
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
@ -3469,7 +3457,6 @@ namespace BTCPayServer.Tests
|
||||
var store2 = (await adminClient.CreateStore(new CreateStoreRequest() { Name = "test2" })).Id;
|
||||
var address1 = Guid.NewGuid().ToString("n").Substring(0, 8);
|
||||
var address2 = Guid.NewGuid().ToString("n").Substring(0, 8);
|
||||
var address3 = Guid.NewGuid().ToString("n").Substring(0, 8);
|
||||
|
||||
Assert.Empty(await adminClient.GetStoreLightningAddresses(store.Id));
|
||||
Assert.Empty(await adminClient.GetStoreLightningAddresses(store2));
|
||||
@ -3497,17 +3484,6 @@ namespace BTCPayServer.Tests
|
||||
await adminClient.RemoveStoreLightningAddress(store2, address2);
|
||||
|
||||
Assert.Empty(await adminClient.GetStoreLightningAddresses(store2));
|
||||
|
||||
var store3 = (await adminClient.CreateStore(new CreateStoreRequest { Name = "test3" })).Id;
|
||||
Assert.Empty(await adminClient.GetStoreLightningAddresses(store3));
|
||||
var metadata = JObject.FromObject(new { test = 123 });
|
||||
await adminClient.AddOrUpdateStoreLightningAddress(store3, address3, new LightningAddressData
|
||||
{
|
||||
InvoiceMetadata = metadata
|
||||
});
|
||||
var lnAddresses = await adminClient.GetStoreLightningAddresses(store3);
|
||||
Assert.Single(lnAddresses);
|
||||
Assert.Equal(metadata, lnAddresses[0].InvoiceMetadata);
|
||||
}
|
||||
|
||||
[Fact(Timeout = 60 * 2 * 1000)]
|
||||
|
@ -18,6 +18,7 @@ using NBitcoin;
|
||||
using NBitcoin.RPC;
|
||||
using OpenQA.Selenium;
|
||||
using OpenQA.Selenium.Chrome;
|
||||
using OpenQA.Selenium.Support.Extensions;
|
||||
using OpenQA.Selenium.Support.UI;
|
||||
using Xunit;
|
||||
|
||||
|
@ -14,6 +14,7 @@ using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.NTag424;
|
||||
using BTCPayServer.Payments;
|
||||
@ -2039,6 +2040,7 @@ namespace BTCPayServer.Tests
|
||||
public async Task CanUsePullPaymentsViaUI()
|
||||
{
|
||||
using var s = CreateSeleniumTester();
|
||||
s.Server.DeleteStore = false;
|
||||
s.Server.ActivateLightning(LightningConnectionType.LndREST);
|
||||
await s.StartAsync();
|
||||
await s.Server.EnsureChannelsSetup();
|
||||
@ -2161,7 +2163,6 @@ namespace BTCPayServer.Tests
|
||||
});
|
||||
s.GoToHome();
|
||||
//offline/external payout test
|
||||
|
||||
var newStore = s.CreateNewStore();
|
||||
s.GenerateWallet("BTC", "", true, true);
|
||||
s.GoToStore(s.StoreId, StoreNavPages.PullPayments);
|
||||
@ -2351,6 +2352,22 @@ namespace BTCPayServer.Tests
|
||||
|
||||
// Simulate a boltcard
|
||||
{
|
||||
// LNURL Withdraw support check with BTC denomination
|
||||
s.GoToStore(s.StoreId, StoreNavPages.PullPayments);
|
||||
s.Driver.FindElement(By.Id("NewPullPayment")).Click();
|
||||
s.Driver.FindElement(By.Id("Name")).SendKeys("TopUpTest");
|
||||
s.Driver.SetCheckbox(By.Id("AutoApproveClaims"), true);
|
||||
s.Driver.FindElement(By.Id("Amount")).Clear();
|
||||
s.Driver.FindElement(By.Id("Amount")).SendKeys("100000");
|
||||
s.Driver.FindElement(By.Id("Currency")).Clear();
|
||||
s.Driver.FindElement(By.Id("Currency")).SendKeys("SATS" + Keys.Enter);
|
||||
s.FindAlertMessage();
|
||||
s.Driver.FindElement(By.LinkText("View")).Click();
|
||||
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
|
||||
|
||||
s.Driver.FindElement(By.CssSelector("#lnurlwithdraw-button")).Click();
|
||||
s.Driver.WaitForElement(By.Id("qr-code-data-input"));
|
||||
lnurl = new Uri(LNURL.LNURL.Parse(s.Driver.FindElement(By.Id("qr-code-data-input")).GetAttribute("value"), out _).ToString().Replace("https", "http"));
|
||||
var db = s.Server.PayTester.GetService<ApplicationDbContextFactory>();
|
||||
var ppid = lnurl.AbsoluteUri.Split("/").Last();
|
||||
var issuerKey = new IssuerKey(SettingsRepositoryExtensions.FixedKey());
|
||||
@ -2365,6 +2382,25 @@ namespace BTCPayServer.Tests
|
||||
// p and c should work so long as no bolt11 has been submitted
|
||||
info = (LNURLWithdrawRequest)await LNURL.LNURL.FetchInformation(boltcardUrl, s.Server.PayTester.HttpClient);
|
||||
info = (LNURLWithdrawRequest)await LNURL.LNURL.FetchInformation(boltcardUrl, s.Server.PayTester.HttpClient);
|
||||
Assert.NotNull(info.PayLink);
|
||||
Assert.StartsWith("lnurlp://", info.PayLink.AbsoluteUri);
|
||||
// Ignore certs issue
|
||||
info.PayLink = new Uri(info.PayLink.AbsoluteUri.Replace("lnurlp://", "http://"), UriKind.Absolute);
|
||||
var payReq = (LNURLPayRequest)await LNURL.LNURL.FetchInformation(info.PayLink, s.Server.PayTester.HttpClient);
|
||||
var callback = await payReq.SendRequest(LightMoney.Satoshis(100), Network.RegTest, s.Server.PayTester.HttpClient);
|
||||
Assert.NotNull(callback.Pr);
|
||||
var pr = BOLT11PaymentRequest.Parse(callback.Pr, Network.RegTest);
|
||||
Assert.Equal(LightMoney.Satoshis(100), pr.MinimumAmount);
|
||||
var res = await s.Server.CustomerLightningD.Pay(callback.Pr);
|
||||
Assert.Equal(PayResult.Ok, res.Result);
|
||||
var ppService = s.Server.PayTester.GetService<PullPaymentHostedService>();
|
||||
var serializer = s.Server.PayTester.GetService<BTCPayNetworkJsonSerializerSettings>();
|
||||
await TestUtils.EventuallyAsync(async () =>
|
||||
{
|
||||
var pp = await ppService.GetPullPayment(ppid, true);
|
||||
Assert.Contains(pp.Payouts.Select(p => p.GetBlob(serializer)), p => p.CryptoAmount == -LightMoney.Satoshis(100).ToUnit(LightMoneyUnit.BTC));
|
||||
});
|
||||
|
||||
var fakeBoltcardUrl = new Uri(Regex.Replace(boltcardUrl.AbsoluteUri, "p=([A-F0-9]{32})", $"p={RandomBytes(16)}"));
|
||||
await Assert.ThrowsAsync<LNUrlException>(() => LNURL.LNURL.FetchInformation(fakeBoltcardUrl, s.Server.PayTester.HttpClient));
|
||||
fakeBoltcardUrl = new Uri(Regex.Replace(boltcardUrl.AbsoluteUri, "c=([A-F0-9]{16})", $"c={RandomBytes(8)}"));
|
||||
@ -2620,9 +2656,9 @@ namespace BTCPayServer.Tests
|
||||
|
||||
// Receipt
|
||||
s.Driver.WaitForElement(By.Id("ReceiptLink")).Click();
|
||||
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"));
|
||||
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"));
|
||||
Assert.Equal(2, items.Count);
|
||||
Assert.Equal(4, sums.Count);
|
||||
Assert.Contains("Manual entry 1", items[0].FindElement(By.CssSelector("th")).Text);
|
||||
@ -2638,33 +2674,6 @@ namespace BTCPayServer.Tests
|
||||
Assert.Contains("Total", sums[3].FindElement(By.CssSelector("th")).Text);
|
||||
Assert.Contains("1 222,21 €", sums[3].FindElement(By.CssSelector("td")).Text);
|
||||
|
||||
// Receipt print
|
||||
s.Driver.FindElement(By.Id("ReceiptLinkPrint")).Click();
|
||||
windows = s.Driver.WindowHandles;
|
||||
Assert.Equal(3, windows.Count);
|
||||
s.Driver.SwitchTo().Window(windows[2]);
|
||||
var paymentDetails = s.Driver.WaitForElement(By.CssSelector("#PaymentDetails table"));
|
||||
items = paymentDetails.FindElements(By.CssSelector("tr.cart-data"));
|
||||
sums = paymentDetails.FindElements(By.CssSelector("tr.sums-data"));
|
||||
Assert.Equal(2, items.Count);
|
||||
Assert.Equal(4, sums.Count);
|
||||
Assert.Contains("Manual entry 1", items[0].FindElement(By.CssSelector(".key")).Text);
|
||||
Assert.Contains("1 234,00 €", items[0].FindElement(By.CssSelector(".val")).Text);
|
||||
Assert.Contains("Manual entry 2", items[1].FindElement(By.CssSelector(".key")).Text);
|
||||
Assert.Contains("0,56 €", items[1].FindElement(By.CssSelector(".val")).Text);
|
||||
Assert.Contains("Subtotal", sums[0].FindElement(By.CssSelector(".key")).Text);
|
||||
Assert.Contains("1 234,56 €", sums[0].FindElement(By.CssSelector(".val")).Text);
|
||||
Assert.Contains("Discount", sums[1].FindElement(By.CssSelector(".key")).Text);
|
||||
Assert.Contains("10% = 123,46 €", sums[1].FindElement(By.CssSelector(".val")).Text);
|
||||
Assert.Contains("Tip", sums[2].FindElement(By.CssSelector(".key")).Text);
|
||||
Assert.Contains("10% = 111,11 €", sums[2].FindElement(By.CssSelector(".val")).Text);
|
||||
Assert.Contains("Total", sums[3].FindElement(By.CssSelector(".key")).Text);
|
||||
Assert.Contains("1 222,21 €", sums[3].FindElement(By.CssSelector(".val")).Text);
|
||||
s.Driver.Close();
|
||||
s.Driver.SwitchTo().Window(windows[1]);
|
||||
s.Driver.Close();
|
||||
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
|
||||
|
||||
// Once more with items
|
||||
s.GoToUrl(editUrl);
|
||||
s.Driver.FindElement(By.Id("ShowItems")).Click();
|
||||
@ -2702,40 +2711,22 @@ namespace BTCPayServer.Tests
|
||||
|
||||
// Receipt
|
||||
s.Driver.WaitForElement(By.Id("ReceiptLink")).Click();
|
||||
cartData = s.Driver.FindElement(By.CssSelector("#CartData table"));
|
||||
items = cartData.FindElements(By.CssSelector("tbody tr"));
|
||||
sums = cartData.FindElements(By.CssSelector("tfoot tr"));
|
||||
|
||||
additionalData = s.Driver.FindElement(By.CssSelector("#AdditionalData table"));
|
||||
items = additionalData.FindElements(By.CssSelector("tbody tr"));
|
||||
sums = additionalData.FindElements(By.CssSelector("tfoot tr"));
|
||||
Assert.Equal(3, items.Count);
|
||||
Assert.Single(sums);
|
||||
Assert.Equal(2, sums.Count);
|
||||
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("Total", sums[0].FindElement(By.CssSelector("th")).Text);
|
||||
Assert.Contains("Subtotal", sums[0].FindElement(By.CssSelector("th")).Text);
|
||||
Assert.Contains("4,23 €", sums[0].FindElement(By.CssSelector("td")).Text);
|
||||
|
||||
// Receipt print
|
||||
s.Driver.FindElement(By.Id("ReceiptLinkPrint")).Click();
|
||||
windows = s.Driver.WindowHandles;
|
||||
Assert.Equal(2, windows.Count);
|
||||
s.Driver.SwitchTo().Window(windows[1]);
|
||||
paymentDetails = s.Driver.WaitForElement(By.CssSelector("#PaymentDetails table"));
|
||||
items = paymentDetails.FindElements(By.CssSelector("tr.cart-data"));
|
||||
sums = paymentDetails.FindElements(By.CssSelector("tr.sums-data"));
|
||||
Assert.Equal(3, items.Count);
|
||||
Assert.Single(sums);
|
||||
Assert.Contains("Black Tea", items[0].FindElement(By.CssSelector(".key")).Text);
|
||||
Assert.Contains("1 x 1,00 € = 1,00 €", items[0].FindElement(By.CssSelector(".val")).Text);
|
||||
Assert.Contains("Green Tea", items[1].FindElement(By.CssSelector(".key")).Text);
|
||||
Assert.Contains("2 x 1,00 € = 2,00 €", items[1].FindElement(By.CssSelector(".val")).Text);
|
||||
Assert.Contains("Manual entry 1", items[2].FindElement(By.CssSelector(".key")).Text);
|
||||
Assert.Contains("1,23 €", items[2].FindElement(By.CssSelector(".val")).Text);
|
||||
Assert.Contains("Total", sums[0].FindElement(By.CssSelector(".key")).Text);
|
||||
Assert.Contains("4,23 €", sums[0].FindElement(By.CssSelector(".val")).Text);
|
||||
s.Driver.Close();
|
||||
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
|
||||
Assert.Contains("Total", sums[1].FindElement(By.CssSelector("th")).Text);
|
||||
Assert.Contains("4,23 €", sums[1].FindElement(By.CssSelector("td")).Text);
|
||||
|
||||
// Guest user can access recent transactions
|
||||
s.GoToHome();
|
||||
@ -2882,9 +2873,9 @@ namespace BTCPayServer.Tests
|
||||
|
||||
// Receipt
|
||||
s.Driver.WaitForElement(By.Id("ReceiptLink")).Click();
|
||||
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"));
|
||||
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"));
|
||||
Assert.Equal(7, items.Count);
|
||||
Assert.Equal(4, sums.Count);
|
||||
Assert.Contains("Black Tea", items[0].FindElement(By.CssSelector("th")).Text);
|
||||
|
@ -272,8 +272,7 @@ 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.wasabiwallet.io/" // returning Forbidden
|
||||
"https://www.coingecko.com" // unhappy service
|
||||
};
|
||||
|
||||
foreach (var match in regex.Matches(text).OfType<Match>())
|
||||
|
@ -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("Integration", "Integration")]
|
||||
[Trait("Selenium", "Selenium")]
|
||||
public async Task CanCreateReports()
|
||||
{
|
||||
using var tester = CreateServerTester(newDb: true);
|
||||
@ -3093,48 +3093,6 @@ 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,6 +18,7 @@ using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using OpenQA.Selenium;
|
||||
using OpenQA.Selenium.Chrome;
|
||||
using OpenQA.Selenium.DevTools.V100.Network;
|
||||
using OpenQA.Selenium.Support.UI;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
@ -89,7 +89,7 @@ services:
|
||||
- merchant_lnd
|
||||
|
||||
selenium:
|
||||
image: selenium/standalone-chrome:125.0
|
||||
image: selenium/standalone-chrome:101.0
|
||||
extra_hosts:
|
||||
- "tests:172.23.0.18"
|
||||
expose:
|
||||
@ -99,7 +99,7 @@ services:
|
||||
custom:
|
||||
|
||||
nbxplorer:
|
||||
image: nicolasdorier/nbxplorer:2.5.4
|
||||
image: nicolasdorier/nbxplorer:2.5.0
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "32838:32838"
|
||||
@ -163,7 +163,7 @@ services:
|
||||
- "bitcoin_datadir:/data"
|
||||
|
||||
customer_lightningd:
|
||||
image: btcpayserver/lightning:v24.05
|
||||
image: btcpayserver/lightning:v24.02.2
|
||||
stop_signal: SIGKILL
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
@ -191,9 +191,8 @@ services:
|
||||
- bitcoind
|
||||
|
||||
merchant_lightningd:
|
||||
image: btcpayserver/lightning:v24.05
|
||||
image: btcpayserver/lightning:v24.02.2
|
||||
stop_signal: SIGKILL
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
EXPOSE_TCP: "true"
|
||||
LIGHTNINGD_CHAIN: "btc"
|
||||
@ -217,7 +216,6 @@ services:
|
||||
- "merchant_lightningd_datadir:/root/.lightning"
|
||||
depends_on:
|
||||
- bitcoind
|
||||
|
||||
postgres:
|
||||
image: postgres:13.4
|
||||
environment:
|
||||
@ -228,7 +226,7 @@ services:
|
||||
- "5432"
|
||||
|
||||
merchant_lnd:
|
||||
image: btcpayserver/lnd:v0.18.1-beta
|
||||
image: btcpayserver/lnd:v0.17.4-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
@ -263,7 +261,7 @@ services:
|
||||
- bitcoind
|
||||
|
||||
customer_lnd:
|
||||
image: btcpayserver/lnd:v0.18.1-beta
|
||||
image: btcpayserver/lnd:v0.17.4-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
@ -310,29 +308,27 @@ services:
|
||||
- "tor_datadir:/home/tor/.tor"
|
||||
- "torrcdir:/usr/local/etc/tor"
|
||||
- "tor_servicesdir:/var/lib/tor/hidden_services"
|
||||
|
||||
monerod:
|
||||
image: btcpayserver/monero:0.18.3.3
|
||||
restart: unless-stopped
|
||||
container_name: xmr_monerod
|
||||
entrypoint: monerod --fixed-difficulty 200 --rpc-bind-ip=0.0.0.0 --confirm-external-bind --rpc-bind-port=18081 --block-notify="/bin/sh ./scripts/notifier.sh -k -X GET https://host.docker.internal:14142/monerolikedaemoncallback/block?cryptoCode=xmr&hash=%s" --testnet --no-igd --hide-my-port --offline --non-interactive
|
||||
volumes:
|
||||
- "monero_data:/home/monero/.bitmonero"
|
||||
ports:
|
||||
- "18081:18081"
|
||||
|
||||
image: btcpayserver/monero:0.18.2.2-5
|
||||
restart: unless-stopped
|
||||
container_name: xmr_monerod
|
||||
entrypoint: sleep 999999
|
||||
# entrypoint: monerod --fixed-difficulty 200 --rpc-bind-ip=0.0.0.0 --confirm-external-bind --rpc-bind-port=18081 --block-notify="/bin/sh ./scripts/notifier.sh -k -X GET https://host.docker.internal:14142/monerolikedaemoncallback/block?cryptoCode=xmr&hash=%s" --testnet --no-igd --hide-my-port --offline
|
||||
volumes:
|
||||
- "monero_data:/home/monero/.bitmonero"
|
||||
ports:
|
||||
- "18081:18081"
|
||||
monero_wallet:
|
||||
image: btcpayserver/monero:0.18.3.3
|
||||
image: btcpayserver/monero:0.18.2.2-5
|
||||
restart: unless-stopped
|
||||
container_name: xmr_wallet_rpc
|
||||
entrypoint: monero-wallet-rpc --testnet --rpc-bind-ip=0.0.0.0 --disable-rpc-login --confirm-external-bind --rpc-bind-port=18082 --non-interactive --trusted-daemon --daemon-address=monerod:18081 --wallet-dir=/wallet --tx-notify="/bin/sh ./scripts/notifier.sh -k -X GET https://host.docker.internal:14142/monerolikedaemoncallback/tx?cryptoCode=xmr&hash=%s"
|
||||
entrypoint: monero-wallet-rpc --testnet --rpc-bind-ip=0.0.0.0 --disable-rpc-login --confirm-external-bind --rpc-bind-port=18082 --non-interactive --trusted-daemon --daemon-address=monerod:18081 --wallet-file=/wallet/wallet.keys --password-file=/wallet/password --tx-notify="/bin/sh ./scripts/notifier.sh -k -X GET https://host.docker.internal:14142/monerolikedaemoncallback/tx?cryptoCode=xmr&hash=%s"
|
||||
ports:
|
||||
- "18082:18082"
|
||||
volumes:
|
||||
- "./monero_wallet:/wallet"
|
||||
depends_on:
|
||||
- monerod
|
||||
|
||||
- monerod
|
||||
litecoind:
|
||||
restart: unless-stopped
|
||||
image: btcpayserver/litecoin:0.18.1
|
||||
|
@ -86,7 +86,7 @@ services:
|
||||
- merchant_lnd
|
||||
|
||||
selenium:
|
||||
image: selenium/standalone-chrome:125.0
|
||||
image: selenium/standalone-chrome:101.0
|
||||
extra_hosts:
|
||||
- "tests:172.23.0.18"
|
||||
expose:
|
||||
@ -96,7 +96,7 @@ services:
|
||||
custom:
|
||||
|
||||
nbxplorer:
|
||||
image: nicolasdorier/nbxplorer:2.5.4
|
||||
image: nicolasdorier/nbxplorer:2.5.0
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "32838:32838"
|
||||
@ -149,7 +149,7 @@ services:
|
||||
- "bitcoin_datadir:/data"
|
||||
|
||||
customer_lightningd:
|
||||
image: btcpayserver/lightning:v24.05
|
||||
image: btcpayserver/lightning:v24.02.2
|
||||
stop_signal: SIGKILL
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
@ -177,9 +177,8 @@ services:
|
||||
- bitcoind
|
||||
|
||||
merchant_lightningd:
|
||||
image: btcpayserver/lightning:v24.05
|
||||
image: btcpayserver/lightning:v24.02.2
|
||||
stop_signal: SIGKILL
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
EXPOSE_TCP: "true"
|
||||
LIGHTNINGD_CHAIN: "btc"
|
||||
@ -214,7 +213,7 @@ services:
|
||||
- "5432"
|
||||
|
||||
merchant_lnd:
|
||||
image: btcpayserver/lnd:v0.18.1-beta
|
||||
image: btcpayserver/lnd:v0.17.4-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
@ -251,7 +250,7 @@ services:
|
||||
- bitcoind
|
||||
|
||||
customer_lnd:
|
||||
image: btcpayserver/lnd:v0.18.1-beta
|
||||
image: btcpayserver/lnd:v0.17.4-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
|
@ -1,199 +0,0 @@
|
||||
#!/bin/bash
|
||||
USERHOST="btcpay.local"
|
||||
BASE="https://localhost:14142"
|
||||
API_BASE="$BASE/api/v1"
|
||||
PASSWORD="rockstar"
|
||||
|
||||
# Ensure we are in the script directory
|
||||
cd "$(dirname "${BASH_SOURCE}")"
|
||||
|
||||
# Create admin user
|
||||
admin_id=$(curl -s -k -X POST -H 'Content-Type: application/json' \
|
||||
-d "{'email': 'admin@$USERHOST', 'password': '$PASSWORD', 'isAdministrator': true }" \
|
||||
"$API_BASE/users" | jq -r '.id')
|
||||
|
||||
printf "Admin ID: %s\n" "$admin_id"
|
||||
|
||||
# Create unlimited access API key
|
||||
admin_api_key=$(curl -s -k -X POST -H 'Content-Type: application/json' \
|
||||
-d "{'permissions': ['unrestricted'], 'label': 'Unrestricted' }" \
|
||||
--user "admin@$USERHOST:$PASSWORD" \
|
||||
"$API_BASE/api-keys" | jq -r '.apiKey')
|
||||
|
||||
printf "Admin API Key: %s\n" "$admin_api_key"
|
||||
|
||||
printf "\n"
|
||||
|
||||
# Create Store Owner
|
||||
owner_id=$(curl -s -k -X POST -H 'Content-Type: application/json' \
|
||||
-d "{'email': 'owner@$USERHOST', 'password': '$PASSWORD', 'isAdministrator': false }" \
|
||||
-H "Authorization: token $admin_api_key" \
|
||||
"$API_BASE/users" | jq -r '.id')
|
||||
|
||||
printf "Store Owner ID: %s\n" "$owner_id"
|
||||
|
||||
# Create Store Manager
|
||||
manager_id=$(curl -s -k -X POST -H 'Content-Type: application/json' \
|
||||
-d "{'email': 'manager@$USERHOST', 'password': '$PASSWORD', 'isAdministrator': false }" \
|
||||
-H "Authorization: token $admin_api_key" \
|
||||
"$API_BASE/users" | jq -r '.id')
|
||||
|
||||
printf "Store Manager ID: %s\n" "$manager_id"
|
||||
|
||||
# Create Store Employee
|
||||
employee_id=$(curl -s -k -X POST -H 'Content-Type: application/json' \
|
||||
-d "{'email': 'employee@$USERHOST', 'password': '$PASSWORD', 'isAdministrator': false }" \
|
||||
-H "Authorization: token $admin_api_key" \
|
||||
"$API_BASE/users" | jq -r '.id')
|
||||
|
||||
printf "Store Employee ID: %s\n" "$employee_id"
|
||||
|
||||
printf "\n"
|
||||
|
||||
# Create Satoshis Steaks store
|
||||
res=$(curl -s -k -X POST -H 'Content-Type: application/json' \
|
||||
-d "{'name': 'Satoshis Steaks', 'checkoutType': 'V2', 'lightningAmountInSatoshi': true, 'onChainWithLnInvoiceFallback': true, 'playSoundOnPayment': true, 'defaultCurrency': 'EUR' }" \
|
||||
-H "Authorization: token $admin_api_key" \
|
||||
"$API_BASE/stores")
|
||||
store_id_satoshis_steaks=$( echo $res | jq -r '.id')
|
||||
if [ -z "${store_id_satoshis_steaks}" ]; then
|
||||
printf "Error creating Satoshis Steaks store: %s\n" "$res"
|
||||
exit 1
|
||||
fi
|
||||
printf "Satoshis Steaks Store ID: %s\n" "$store_id_satoshis_steaks"
|
||||
|
||||
# Create Hot Wallet for Satoshis Steaks store
|
||||
wallet_enabled_satoshis_steaks=$(curl -s -k -X PUT -H 'Content-Type: application/json' \
|
||||
-d "{'enabled': true, 'config': 'tpubDC2mCtL7EPhey3qRgHXmKQRraxXgiuSTkHdJbDW22xLK1YMXy8jdEq7jx2UN5z1wU5xBWWZdSpAobG1bbZBTR4f8R3AjL31EzoexpngKUXM' }" \
|
||||
-H "Authorization: token $admin_api_key" \
|
||||
"$API_BASE/stores/$store_id_satoshis_steaks/payment-methods/BTC-CHAIN")
|
||||
|
||||
# Create Internal Node connection for Satoshis Steaks store
|
||||
ln_enabled_satoshis_steaks=$(curl -s -k -X PUT -H 'Content-Type: application/json' \
|
||||
-d "{'enabled': true, 'config': { 'connectionString': 'Internal Node' } }" \
|
||||
-H "Authorization: token $admin_api_key" \
|
||||
"$API_BASE/stores/$store_id_satoshis_steaks/payment-methods/BTC-LN")
|
||||
|
||||
# LNURL settings
|
||||
curl -s -k -X PUT -H 'Content-Type: application/json' \
|
||||
-d "{'enabled': true, 'config': { 'lud12Enabled': true, 'useBech32Scheme': true } }" \
|
||||
-H "Authorization: token $admin_api_key" \
|
||||
"$API_BASE/stores/$store_id_satoshis_steaks/payment-methods/BTC-LNURL" >/dev/null 2>&1
|
||||
|
||||
# Fund Satoshis Steaks wallet
|
||||
btcaddress_satoshis_steaks=$(curl -s -k -X GET -H 'Content-Type: application/json' \
|
||||
-H "Authorization: token $admin_api_key" \
|
||||
"$API_BASE/stores/$store_id_satoshis_steaks/payment-methods/onchain/BTC/wallet/address" | jq -r '.address')
|
||||
|
||||
./docker-bitcoin-cli.sh sendtoaddress "$btcaddress_satoshis_steaks" 6.15 >/dev/null 2>&1
|
||||
|
||||
printf "\n"
|
||||
|
||||
# Add store users to Satoshis Steaks store
|
||||
curl -s -k -X POST -H 'Content-Type: application/json' \
|
||||
-d "{'userId': '$owner_id', 'role': 'Owner' }" \
|
||||
-H "Authorization: token $admin_api_key" \
|
||||
"$API_BASE/stores/$store_id_satoshis_steaks/users" >/dev/null 2>&1
|
||||
|
||||
curl -s -k -X POST -H 'Content-Type: application/json' \
|
||||
-d "{'userId': '$manager_id', 'role': 'Manager' }" \
|
||||
-H "Authorization: token $admin_api_key" \
|
||||
"$API_BASE/stores/$store_id_satoshis_steaks/users" >/dev/null 2>&1
|
||||
|
||||
curl -s -k -X POST -H 'Content-Type: application/json' \
|
||||
-d "{'userId': '$employee_id', 'role': 'Employee' }" \
|
||||
-H "Authorization: token $admin_api_key" \
|
||||
"$API_BASE/stores/$store_id_satoshis_steaks/users" >/dev/null 2>&1
|
||||
|
||||
# Create Nakamoto Nuggets store
|
||||
store_id_nakamoto_nuggets=$(curl -s -k -X POST -H 'Content-Type: application/json' \
|
||||
-d "{'name': 'Nakamoto Nuggets', 'checkoutType': 'V2', 'lightningAmountInSatoshi': true, 'onChainWithLnInvoiceFallback': true, 'playSoundOnPayment': true }" \
|
||||
-H "Authorization: token $admin_api_key" \
|
||||
"$API_BASE/stores" | jq -r '.id')
|
||||
|
||||
printf "Nakamoto Nuggets Store ID: %s\n" "$store_id_nakamoto_nuggets"
|
||||
|
||||
# Create Hot Wallet for Nakamoto Nuggets store
|
||||
# Seed: "resist camera spread better amazing cliff giraffe duty betray throw twelve father"
|
||||
wallet_enabled_nakamoto_nuggets=$(curl -s -k -X PUT -H 'Content-Type: application/json' \
|
||||
-d "{'enabled': true, 'config': 'tpubDD79XF4pzhmPSJ9AyUay9YbXAeD1c6nkUqC32pnKARJH6Ja5hGUfGc76V82ahXpsKqN6UcSGXMkzR34aZq4W23C6DAdZFaVrzWqzj24F8BC' }" \
|
||||
-H "Authorization: token $admin_api_key" \
|
||||
"$API_BASE/stores/$store_id_nakamoto_nuggets/payment-methods/BTC-CHAIN")
|
||||
|
||||
# Connect Nakamoto Nuggets with Merchant LND Lightning node
|
||||
curl -s -k -X PUT -H 'Content-Type: application/json' \
|
||||
-d "{'enabled': true, 'config': { 'connectionString': 'type=lnd-rest;server=http://lnd:lnd@127.0.0.1:35531/;allowinsecure=true' }}" \
|
||||
-H "Authorization: token $admin_api_key" \
|
||||
"$API_BASE/stores/$store_id_nakamoto_nuggets/payment-methods/BTC-LN" >/dev/null 2>&1
|
||||
|
||||
# LNURL settings
|
||||
curl -s -k -X PUT -H 'Content-Type: application/json' \
|
||||
-d "{'enabled': true, 'config': { 'lud12Enabled': true, 'useBech32Scheme': true } }" \
|
||||
-H "Authorization: token $admin_api_key" \
|
||||
"$API_BASE/stores/$store_id_nakamoto_nuggets/payment-methods/BTC-LNURL" >/dev/null 2>&1
|
||||
|
||||
# Add store users to Nakamoto Nuggets store
|
||||
curl -s -k -X POST -H 'Content-Type: application/json' \
|
||||
-d "{'userId': '$owner_id', 'role': 'Owner' }" \
|
||||
-H "Authorization: token $admin_api_key" \
|
||||
"$API_BASE/stores/$store_id_nakamoto_nuggets/users" >/dev/null 2>&1
|
||||
|
||||
curl -s -k -X POST -H 'Content-Type: application/json' \
|
||||
-d "{'userId': '$manager_id', 'role': 'Manager' }" \
|
||||
-H "Authorization: token $admin_api_key" \
|
||||
"$API_BASE/stores/$store_id_nakamoto_nuggets/users" >/dev/null 2>&1
|
||||
|
||||
curl -s -k -X POST -H 'Content-Type: application/json' \
|
||||
-d "{'userId': '$employee_id', 'role': 'Employee' }" \
|
||||
-H "Authorization: token $admin_api_key" \
|
||||
"$API_BASE/stores/$store_id_nakamoto_nuggets/users" >/dev/null 2>&1
|
||||
|
||||
# Create Nakamoto Nuggets keypad app
|
||||
keypad_app_id_nakamoto_nuggets=$(curl -s -k -X POST -H 'Content-Type: application/json' \
|
||||
-d "{'appName': 'Keypad', 'title': 'Keypad', 'defaultView': 'light', 'currency': 'SATS' }" \
|
||||
-H "Authorization: token $admin_api_key" \
|
||||
"$API_BASE/stores/{$store_id_nakamoto_nuggets}/apps/pos" | jq -r '.id')
|
||||
|
||||
printf "Nakamoto Nuggets Keypad POS ID: %s\n" "$keypad_app_id_nakamoto_nuggets"
|
||||
|
||||
# Create Nakamoto Nuggets cart app
|
||||
cart_app_id_nakamoto_nuggets=$(curl -s -k -X POST -H 'Content-Type: application/json' \
|
||||
-d "{'appName': 'Cart', 'title': 'Cart', 'defaultView': 'cart', 'template': '[{\"id\":\"birell beer\",\"image\":\"https://i.imgur.com/r8N6rTU.png\",\"priceType\":\"Fixed\",\"price\":\"20\",\"title\":\"Birell Beer\",\"disabled\":false},{\"id\":\"flavoured birell beer\",\"image\":\"https://i.imgur.com/de43iUd.png\",\"priceType\":\"Fixed\",\"price\":\"20\",\"title\":\"Flavoured Birell Beer\",\"disabled\":false},{\"id\":\"wostok\",\"image\":\"https://i.imgur.com/gP6zqub.png\",\"priceType\":\"Fixed\",\"price\":\"25\",\"title\":\"Wostok\",\"disabled\":false},{\"id\":\"pilsner beer\",\"image\":\"https://i.imgur.com/M4EEaEP.png\",\"priceType\":\"Fixed\",\"price\":\"30\",\"title\":\"Pilsner Beer\",\"disabled\":false},{\"id\":\"club mate\",\"image\":\"https://i.imgur.com/H9p9Xwc.png\",\"priceType\":\"Fixed\",\"price\":\"35\",\"title\":\"Club Mate\",\"disabled\":false},{\"id\":\"seicha / selo / koka\",\"image\":\"https://i.imgur.com/ReW3RKe.png\",\"priceType\":\"Fixed\",\"price\":\"35\",\"title\":\"Seicha / Selo / Koka\",\"disabled\":false},{\"id\":\"limonada z kopanic\",\"image\":\"https://i.imgur.com/2Xb35Zs.png\",\"priceType\":\"Fixed\",\"price\":\"40\",\"title\":\"Limonada z Kopanic\",\"disabled\":false},{\"id\":\"mellow drink\",\"image\":\"https://i.imgur.com/ilDUWiP.png\",\"priceType\":\"Fixed\",\"price\":\"40\",\"title\":\"Mellow Drink\",\"disabled\":false},{\"id\":\"bacilli drink\",\"image\":\"https://i.imgur.com/3BsCLgG.png\",\"priceType\":\"Fixed\",\"price\":\"40\",\"title\":\"Bacilli Drink\",\"disabled\":false},{\"description\":\"\",\"id\":\"vincentka\",\"image\":\"https://i.imgur.com/99reAEg.png\",\"priceType\":\"Fixed\",\"price\":\"20\",\"title\":\"Vincentka\",\"disabled\":false,\"index\":\"-1\"},{\"id\":\"kinder bar\",\"image\":\"https://i.imgur.com/va9i6SQ.png\",\"priceType\":\"Fixed\",\"price\":\"20\",\"title\":\"Kinder bar\",\"disabled\":false},{\"id\":\"nutrend bar\",\"image\":\"https://i.imgur.com/zzdIup0.png\",\"priceType\":\"Fixed\",\"price\":\"15\",\"title\":\"Nutrend bar\",\"disabled\":false},{\"id\":\"yoghurt\",\"image\":\"https://i.imgur.com/biP4Dr8.png\",\"priceType\":\"Fixed\",\"price\":\"20\",\"title\":\"Yoghurt\",\"disabled\":false},{\"id\":\"mini magnum\",\"image\":\"https://i.imgur.com/tveN4Aa.png\",\"priceType\":\"Fixed\",\"price\":\"35\",\"title\":\"Mini Magnum\",\"disabled\":false},{\"description\":\"\",\"id\":\"nanuk do:pusy\",\"image\":\"https://i.imgur.com/EzZN6lV.png\",\"priceType\":\"Fixed\",\"price\":\"30\",\"title\":\"Nanuk DO:PUSY\",\"disabled\":false,\"index\":\"-1\"},{\"id\":\"alpro dessert\",\"image\":\"https://i.imgur.com/L0MHkcs.png\",\"priceType\":\"Fixed\",\"price\":\"30\",\"title\":\"Alpro dessert\",\"disabled\":false},{\"id\":\"mixitka bar\",\"image\":\"https://i.imgur.com/gHuTGK3.png\",\"priceType\":\"Fixed\",\"price\":\"30\",\"title\":\"Mixitka bar\",\"disabled\":false},{\"id\":\"instatni polivka\",\"image\":\"https://cdn.rohlik.cz/images/grocery/products/722313/722313-1598298944.jpg\",\"priceType\":\"Fixed\",\"price\":\"15\",\"title\":\"Instatni polivka\",\"disabled\":false},{\"id\":\"m&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
|
@ -22,6 +22,18 @@
|
||||
<EmbeddedResource Remove="wwwroot\vendor\jquery-nice-select\**" />
|
||||
<None Remove="Build\**" />
|
||||
<None Remove="wwwroot\vendor\jquery-nice-select\**" />
|
||||
<Content Update="Plugins\BoltcardBalance\Views\ScanCard.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Plugins\BoltcardFactory\Views\UpdateBoltcardFactory.cshtml">
|
||||
<Pack>false</Pack>
|
||||
</Content>
|
||||
<Content Update="Plugins\BoltcardFactory\Views\ViewBoltcardFactory.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Plugins\BoltcardTopUp\Views\ScanCard.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Views\UIStorePullPayments\NewPullPayment.cshtml">
|
||||
<Pack>false</Pack>
|
||||
</Content>
|
||||
@ -51,8 +63,8 @@
|
||||
<PackageReference Include="BIP78.Sender" Version="0.2.2" />
|
||||
<PackageReference Include="BTCPayServer.Hwi" Version="2.0.2" />
|
||||
<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="CsvHelper" Version="15.0.5" />
|
||||
<PackageReference Include="Dapper" Version="2.1.28" />
|
||||
<PackageReference Include="Fido2" Version="2.0.2" />
|
||||
<PackageReference Include="Fido2.AspNet" Version="2.0.2" />
|
||||
<PackageReference Include="LNURL" Version="0.0.34" />
|
||||
@ -77,8 +89,8 @@
|
||||
<PackageReference Include="TwentyTwenty.Storage.Azure" Version="2.12.1" />
|
||||
<PackageReference Include="TwentyTwenty.Storage.Google" Version="2.12.1" />
|
||||
<PackageReference Include="TwentyTwenty.Storage.Local" Version="2.12.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="8.0.6" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.7" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -34,36 +34,34 @@
|
||||
}
|
||||
else if (Model.Invoices.Any())
|
||||
{
|
||||
<div class="table-responsive mt-3 mb-0">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead>
|
||||
<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)
|
||||
{
|
||||
<tr>
|
||||
<th class="w-125px">Date</th>
|
||||
<th class="text-nowrap">Invoice Id</th>
|
||||
<th>Status</th>
|
||||
<th class="text-end">Amount</th>
|
||||
<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>
|
||||
</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>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -31,63 +31,61 @@
|
||||
}
|
||||
else if (Model.Transactions.Any())
|
||||
{
|
||||
<div class="table-responsive mt-3 mb-0">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead>
|
||||
<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)
|
||||
{
|
||||
<tr>
|
||||
<th class="w-125px">Date</th>
|
||||
<th>Transaction</th>
|
||||
<th>Labels</th>
|
||||
<th class="text-end">Amount</th>
|
||||
<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>
|
||||
</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>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
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" id="@Model.Id" data-text="@Model.Text">
|
||||
<span class="truncate-center @classes"@(!string.IsNullOrEmpty(Model.Id) ? $"id={Model.Id}" : null) data-text=@Safe.Json(Model.Text)>
|
||||
@if (Model.IsVue)
|
||||
{
|
||||
<span class="truncate-center-truncated" data-bs-toggle="tooltip" :title="@Model.Text">
|
||||
<span class="truncate-center-truncated" data-bs-toggle="tooltip" :title=@Safe.Json(Model.Text)>
|
||||
@if (Model.Elastic)
|
||||
{
|
||||
<span class="truncate-center-start" v-text="@Model.Text"></span>
|
||||
<span class="truncate-center-start" v-text=@Safe.Json(Model.Text)></span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="truncate-center-start" v-text="@(Model.Text).length > 2 * @(Model.Padding) ? (@(Model.Text).slice(0, @(Model.Padding)) + '…') : @(Model.Text)"></span>
|
||||
<span class="truncate-center-start" v-text=@Safe.Json($"{Model.Text}.slice(0, {Model.Padding})")></span>
|
||||
<span>…</span>
|
||||
}
|
||||
<span class="truncate-center-end" v-text="@(Model.Text).slice(-@(Model.Padding))" v-if="@(Model.Text).length > 2 * @(Model.Padding)"></span>
|
||||
<span class="truncate-center-end" v-text=@Safe.Json($"{Model.Text}.slice(-{Model.Padding})")></span>
|
||||
</span>
|
||||
<span class="truncate-center-text" v-text="@Model.Text"></span>
|
||||
<span class="truncate-center-text" v-text=@Safe.Json(Model.Text)></span>
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -35,13 +35,13 @@
|
||||
}
|
||||
@if (Model.Copy)
|
||||
{
|
||||
<button type="button" class="btn btn-link p-0" @(prefix)data-clipboard="@Model.Text">
|
||||
<button type="button" class="btn btn-link p-0" @(Model.IsVue ? ":" : string.Empty)data-clipboard=@Safe.Json(Model.Text)>
|
||||
<vc:icon symbol="copy" />
|
||||
</button>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(Model.Link))
|
||||
{
|
||||
<a @(prefix)href="@Model.Link" rel="noreferrer noopener" target="_blank">
|
||||
<a @(Model.IsVue ? ":" : "")href="@Model.Link" rel="noreferrer noopener" target="_blank">
|
||||
<vc:icon symbol="info" />
|
||||
</a>
|
||||
}
|
||||
|
@ -213,7 +213,6 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
}
|
||||
|
||||
[HttpDelete("~/api/v1/apps/{appId}")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public async Task<IActionResult> DeleteApp(string appId)
|
||||
{
|
||||
var app = await _appService.GetApp(appId, null, includeArchived: true);
|
||||
|
@ -401,14 +401,6 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
|
||||
var accounting = invoicePaymentMethod.Calculate();
|
||||
var cryptoPaid = accounting.Paid;
|
||||
var dueAmount = accounting.TotalDue;
|
||||
|
||||
// If no payment, but settled and marked, assume it has been fully paid
|
||||
if (cryptoPaid is 0 && invoice is { Status: InvoiceStatusLegacy.Confirmed or InvoiceStatusLegacy.Complete, ExceptionStatus: InvoiceExceptionStatus.Marked })
|
||||
{
|
||||
cryptoPaid = accounting.TotalDue;
|
||||
dueAmount = 0;
|
||||
}
|
||||
var cdCurrency = _currencyNameTable.GetCurrencyData(invoice.Currency, true);
|
||||
var paidCurrency = Math.Round(cryptoPaid * invoicePaymentMethod.Rate, cdCurrency.Divisibility);
|
||||
var rateResult = await _rateProvider.FetchRate(
|
||||
@ -476,6 +468,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
|
||||
var dueAmount = accounting.TotalDue;
|
||||
createPullPayment.Currency = cryptoCode;
|
||||
createPullPayment.Amount = Math.Round(paidAmount - dueAmount, appliedDivisibility);
|
||||
createPullPayment.AutoApproveClaims = true;
|
||||
|
@ -24,9 +24,7 @@ using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin.DataEncoders;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using MarkPayoutRequest = BTCPayServer.HostedServices.MarkPayoutRequest;
|
||||
|
||||
@ -199,7 +197,6 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
if (pp is null)
|
||||
return PullPaymentNotFound();
|
||||
var issuerKey = await _settingsRepository.GetIssuerKey(_env);
|
||||
BoltcardPICCData? picc = null;
|
||||
|
||||
// LNURLW is used by deeplinks
|
||||
if (request?.LNURLW is not null)
|
||||
@ -215,12 +212,11 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
ModelState.AddModelError(nameof(request.LNURLW), "The LNURLW should contains a 'p=' parameter");
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
if (issuerKey.TryDecrypt(p) is not BoltcardPICCData o)
|
||||
if (issuerKey.TryDecrypt(p) is not BoltcardPICCData picc)
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.LNURLW), "The LNURLW 'p=' parameter cannot be decrypted");
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
picc = o;
|
||||
request.UID = picc.Uid;
|
||||
}
|
||||
|
||||
@ -242,54 +238,8 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
_ => request.OnExisting
|
||||
};
|
||||
|
||||
BoltcardKeys keys;
|
||||
int version;
|
||||
|
||||
var registration = await _dbContextFactory.GetBoltcardRegistration(issuerKey, request.UID);
|
||||
if (request.OnExisting is OnExistingBehavior.UpdateVersion ||
|
||||
(request.OnExisting is null && registration?.PullPaymentId is null))
|
||||
{
|
||||
version = await _dbContextFactory.LinkBoltcardToPullPayment(pullPaymentId, issuerKey, request.UID, request.OnExisting);
|
||||
keys = issuerKey.CreatePullPaymentCardKey(request.UID, version, pullPaymentId).DeriveBoltcardKeys(issuerKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (registration?.PullPaymentId is null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.UID), "This card isn't registered");
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
if (pullPaymentId != registration.PullPaymentId)
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.UID), "This card is registered on a different pull payment");
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
var ppId = registration.PullPaymentId;
|
||||
version = registration.Version;
|
||||
int retryCount = 0;
|
||||
retry:
|
||||
keys = issuerKey.CreatePullPaymentCardKey(request!.UID, version, ppId).DeriveBoltcardKeys(issuerKey);
|
||||
|
||||
// The server version may be higher than the card.
|
||||
// If that is the case, let's try a few versions until we find the right one
|
||||
// by checking c.
|
||||
if (request?.LNURLW is { } lnurlw &&
|
||||
ExtractC(lnurlw) is string c &&
|
||||
picc is not null)
|
||||
{
|
||||
if (!keys.AuthenticationKey.CheckSunMac(c, picc))
|
||||
{
|
||||
retryCount++;
|
||||
version--;
|
||||
if (version < 0 || retryCount > 5)
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.UID), "Unable to get keys of this card, it might be caused by a version mismatch");
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
goto retry;
|
||||
}
|
||||
}
|
||||
}
|
||||
var version = await _dbContextFactory.LinkBoltcardToPullPayment(pullPaymentId, issuerKey, request.UID, request.OnExisting);
|
||||
var keys = issuerKey.CreatePullPaymentCardKey(request.UID, version, pullPaymentId).DeriveBoltcardKeys(issuerKey);
|
||||
|
||||
var boltcardUrl = Url.Action(nameof(UIBoltcardController.GetWithdrawRequest), "UIBoltcard");
|
||||
boltcardUrl = Request.GetAbsoluteUri(boltcardUrl);
|
||||
@ -308,7 +258,7 @@ retry:
|
||||
return Ok(resp);
|
||||
}
|
||||
|
||||
public static string? Extract(string? url, string param, int size)
|
||||
private string? ExtractP(string? url)
|
||||
{
|
||||
if (url is null || !Uri.TryCreate(url, UriKind.Absolute, out var uri))
|
||||
return null;
|
||||
@ -316,13 +266,11 @@ retry:
|
||||
if (num == -1)
|
||||
return null;
|
||||
string input = uri.AbsoluteUri.Substring(num);
|
||||
Match match = Regex.Match(input, param + "=([a-f0-9A-F]{" + size + "})");
|
||||
Match match = Regex.Match(input, "p=([a-f0-9A-F]{32})");
|
||||
if (!match.Success)
|
||||
return null;
|
||||
return match.Groups[1].Value;
|
||||
}
|
||||
public static string? ExtractP(string? url) => Extract(url, "p", 32);
|
||||
public static string? ExtractC(string? url) => Extract(url, "c", 16);
|
||||
|
||||
[HttpGet("~/api/v1/pull-payments/{pullPaymentId}")]
|
||||
[AllowAnonymous]
|
||||
|
@ -1,5 +1,4 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
@ -9,8 +8,6 @@ using BTCPayServer.Data;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using AuthenticationSchemes = BTCPayServer.Abstractions.Constants.AuthenticationSchemes;
|
||||
using LightningAddressData = BTCPayServer.Client.Models.LightningAddressData;
|
||||
|
||||
@ -34,13 +31,12 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
var blob = data.GetBlob();
|
||||
if (blob is null)
|
||||
return new LightningAddressData();
|
||||
return new LightningAddressData
|
||||
return new LightningAddressData()
|
||||
{
|
||||
Username = data.Username,
|
||||
Max = blob.Max,
|
||||
Min = blob.Min,
|
||||
CurrencyCode = blob.CurrencyCode,
|
||||
InvoiceMetadata = blob.InvoiceMetadata
|
||||
CurrencyCode = blob.CurrencyCode
|
||||
};
|
||||
}
|
||||
|
||||
@ -87,17 +83,16 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
ModelState.AddModelError(nameof(data.Min), "Minimum must be greater than 0 if provided.");
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
|
||||
if (await _lightningAddressService.Set(new Data.LightningAddressData
|
||||
|
||||
if (await _lightningAddressService.Set(new Data.LightningAddressData()
|
||||
{
|
||||
StoreDataId = storeId,
|
||||
Username = username
|
||||
}.SetBlob(new LightningAddressDataBlob
|
||||
}.SetBlob(new LightningAddressDataBlob()
|
||||
{
|
||||
Max = data.Max,
|
||||
Min = data.Min,
|
||||
CurrencyCode = data.CurrencyCode,
|
||||
InvoiceMetadata = data.InvoiceMetadata
|
||||
CurrencyCode = data.CurrencyCode
|
||||
})))
|
||||
{
|
||||
return await GetStoreLightningAddress(storeId, username);
|
||||
|
@ -14,11 +14,20 @@ using System.Threading;
|
||||
using System;
|
||||
using NBitcoin.DataEncoders;
|
||||
using System.Text.Json.Serialization;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Lightning;
|
||||
using System.Reflection.Metadata;
|
||||
|
||||
namespace BTCPayServer.Controllers;
|
||||
|
||||
public class UIBoltcardController : Controller
|
||||
{
|
||||
private readonly PullPaymentHostedService _ppService;
|
||||
private readonly StoreRepository _storeRepository;
|
||||
|
||||
public class BoltcardSettings
|
||||
{
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.HexJsonConverter))]
|
||||
@ -28,11 +37,15 @@ public class UIBoltcardController : Controller
|
||||
UILNURLController lnUrlController,
|
||||
SettingsRepository settingsRepository,
|
||||
ApplicationDbContextFactory contextFactory,
|
||||
PullPaymentHostedService ppService,
|
||||
StoreRepository storeRepository,
|
||||
BTCPayServerEnvironment env)
|
||||
{
|
||||
LNURLController = lnUrlController;
|
||||
SettingsRepository = settingsRepository;
|
||||
ContextFactory = contextFactory;
|
||||
_ppService = ppService;
|
||||
_storeRepository = storeRepository;
|
||||
Env = env;
|
||||
}
|
||||
|
||||
@ -41,6 +54,64 @@ public class UIBoltcardController : Controller
|
||||
public ApplicationDbContextFactory ContextFactory { get; }
|
||||
public BTCPayServerEnvironment Env { get; }
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpGet("~/boltcard/pay")]
|
||||
public async Task<IActionResult> GetPayRequest([FromQuery] string? p, [FromQuery] long? amount = null)
|
||||
{
|
||||
var issuerKey = await SettingsRepository.GetIssuerKey(Env);
|
||||
var piccData = issuerKey.TryDecrypt(p);
|
||||
if (piccData is null)
|
||||
return BadRequest(new LNUrlStatusResponse { Status = "ERROR", Reason = "Invalid PICCData" });
|
||||
|
||||
piccData = new BoltcardPICCData(piccData.Uid, int.MaxValue - 10); // do not check the counter
|
||||
var registration = await ContextFactory.GetBoltcardRegistration(issuerKey, piccData, false);
|
||||
var pp = await _ppService.GetPullPayment(registration!.PullPaymentId, false);
|
||||
var store = await _storeRepository.FindStore(pp.StoreId);
|
||||
|
||||
var lnUrlMetadata = new Dictionary<string, string>();
|
||||
lnUrlMetadata.Add("text/plain", "Boltcard Top-Up");
|
||||
var payRequest = new LNURLPayRequest
|
||||
{
|
||||
Tag = "payRequest",
|
||||
MinSendable = LightMoney.Satoshis(1.0m),
|
||||
MaxSendable = LightMoney.FromUnit(6.12m, LightMoneyUnit.BTC),
|
||||
Callback = new Uri(GetPayLink(p, Request.Scheme), UriKind.Absolute),
|
||||
CommentAllowed = 0
|
||||
};
|
||||
payRequest.Metadata = Newtonsoft.Json.JsonConvert.SerializeObject(lnUrlMetadata.Select(kv => new[] { kv.Key, kv.Value }));
|
||||
if (amount is null)
|
||||
return Ok(payRequest);
|
||||
|
||||
var cryptoCode = "BTC";
|
||||
|
||||
var currency = "BTC";
|
||||
var invoiceAmount = LightMoney.FromUnit(amount.Value, LightMoneyUnit.MilliSatoshi).ToUnit(LightMoneyUnit.BTC);
|
||||
|
||||
if (pp.GetBlob().Currency == "SATS")
|
||||
{
|
||||
currency = "SATS";
|
||||
invoiceAmount = LightMoney.FromUnit(amount.Value, LightMoneyUnit.MilliSatoshi).ToUnit(LightMoneyUnit.Satoshi);
|
||||
}
|
||||
|
||||
LNURLController.ControllerContext.HttpContext = HttpContext;
|
||||
var result = await LNURLController.GetLNURLRequest(
|
||||
cryptoCode,
|
||||
store,
|
||||
store.GetStoreBlob(),
|
||||
new CreateInvoiceRequest()
|
||||
{
|
||||
Currency = currency,
|
||||
Amount = invoiceAmount
|
||||
},
|
||||
payRequest,
|
||||
lnUrlMetadata,
|
||||
[PullPaymentHostedService.GetInternalTag(pp.Id)]);
|
||||
if (result is not OkObjectResult ok || ok.Value is not LNURLPayRequest payRequest2)
|
||||
return result;
|
||||
payRequest = payRequest2;
|
||||
var invoiceId = payRequest.Callback.AbsoluteUri.Split('/').Last();
|
||||
return await LNURLController.GetLNURLForInvoice(invoiceId, cryptoCode, amount.Value, null);
|
||||
}
|
||||
[AllowAnonymous]
|
||||
[HttpGet("~/boltcard")]
|
||||
public async Task<IActionResult> GetWithdrawRequest([FromQuery] string? p, [FromQuery] string? c, [FromQuery] string? pr, [FromQuery] string? k1, CancellationToken cancellationToken)
|
||||
@ -65,6 +136,16 @@ public class UIBoltcardController : Controller
|
||||
if (!cardKey.CheckSunMac(c, piccData))
|
||||
return BadRequest(new LNUrlStatusResponse { Status = "ERROR", Reason = "Replayed or expired query" });
|
||||
LNURLController.ControllerContext.HttpContext = HttpContext;
|
||||
return await LNURLController.GetLNURLForPullPayment("BTC", registration.PullPaymentId, pr, $"{p}-{c}", cancellationToken);
|
||||
var res = await LNURLController.GetLNURLForPullPayment("BTC", registration.PullPaymentId, pr, $"{p}-{c}", cancellationToken);
|
||||
if (res is not OkObjectResult ok || ok.Value is not LNURLWithdrawRequest withdrawRequest)
|
||||
return res;
|
||||
var paylink = GetPayLink(p, "lnurlp");
|
||||
withdrawRequest.PayLink = new Uri(paylink, UriKind.Absolute);
|
||||
return res;
|
||||
}
|
||||
|
||||
private string GetPayLink(string? p, string scheme)
|
||||
{
|
||||
return Url.Action(nameof(GetPayRequest), "UIBoltcard", new { p }, scheme)!;
|
||||
}
|
||||
}
|
||||
|
@ -162,10 +162,10 @@ namespace BTCPayServer.Controllers
|
||||
model.Overpaid = details.Overpaid;
|
||||
model.StillDue = details.StillDue;
|
||||
model.HasRates = details.HasRates;
|
||||
|
||||
if (additionalData.TryGetValue("receiptData", out object? receiptData))
|
||||
|
||||
if (additionalData.ContainsKey("receiptData"))
|
||||
{
|
||||
model.ReceiptData = (Dictionary<string, object>)receiptData;
|
||||
model.ReceiptData = (Dictionary<string, object>)additionalData["receiptData"];
|
||||
additionalData.Remove("receiptData");
|
||||
}
|
||||
|
||||
@ -213,7 +213,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
InvoiceId = i.Id,
|
||||
OrderId = i.Metadata?.OrderId,
|
||||
RedirectUrl = i.RedirectURL?.AbsoluteUri ?? i.Metadata?.OrderUrl,
|
||||
OrderUrl = i.Metadata?.OrderUrl,
|
||||
Status = i.Status.ToModernStatus(),
|
||||
Currency = i.Currency,
|
||||
Timestamp = i.InvoiceTime,
|
||||
@ -226,42 +226,15 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
JToken? receiptData = null;
|
||||
i.Metadata?.AdditionalData?.TryGetValue("receiptData", out receiptData);
|
||||
|
||||
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);
|
||||
}
|
||||
@ -368,11 +341,6 @@ namespace BTCPayServer.Controllers
|
||||
accounting = paymentMethod.Calculate();
|
||||
cryptoPaid = accounting.Paid;
|
||||
dueAmount = accounting.TotalDue;
|
||||
if (cryptoPaid is 0 && invoice is { Status: InvoiceStatusLegacy.Confirmed or InvoiceStatusLegacy.Complete, ExceptionStatus: InvoiceExceptionStatus.Marked })
|
||||
{
|
||||
cryptoPaid = accounting.TotalDue;
|
||||
dueAmount = 0;
|
||||
}
|
||||
paidAmount = cryptoPaid.RoundToSignificant(appliedDivisibility);
|
||||
}
|
||||
|
||||
@ -1105,7 +1073,7 @@ namespace BTCPayServer.Controllers
|
||||
storeIds.Add(i);
|
||||
}
|
||||
model.Search = fs;
|
||||
model.SearchText = fs.TextCombined;
|
||||
model.SearchText = fs.TextSearch;
|
||||
|
||||
var apps = await _appService.GetAllApps(GetUserId(), false, storeId);
|
||||
InvoiceQuery invoiceQuery = GetInvoiceQuery(fs, apps, timezoneOffset);
|
||||
|
@ -26,6 +26,7 @@ using BTCPayServer.Plugins.PointOfSale.Models;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.PaymentRequests;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using LNURL;
|
||||
@ -90,14 +91,11 @@ namespace BTCPayServer
|
||||
_pluginHookService = pluginHookService;
|
||||
_invoiceActivator = invoiceActivator;
|
||||
}
|
||||
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
[HttpGet("withdraw/pp/{pullPaymentId}")]
|
||||
public Task<IActionResult> GetLNURLForPullPayment(string cryptoCode, string pullPaymentId, [FromQuery] string pr, CancellationToken cancellationToken)
|
||||
{
|
||||
return GetLNURLForPullPayment(cryptoCode, pullPaymentId, pr, pullPaymentId, cancellationToken);
|
||||
}
|
||||
|
||||
[NonAction]
|
||||
internal async Task<IActionResult> GetLNURLForPullPayment(string cryptoCode, string pullPaymentId, string pr, string k1, CancellationToken cancellationToken)
|
||||
{
|
||||
@ -299,11 +297,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
|
||||
{
|
||||
@ -315,7 +313,6 @@ 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)
|
||||
{
|
||||
@ -330,7 +327,7 @@ namespace BTCPayServer
|
||||
store.GetStoreBlob(),
|
||||
createInvoice,
|
||||
additionalTags: new List<string> { AppService.GetAppInternalTag(appId) },
|
||||
allowOverpay: allowOverpay);
|
||||
allowOverpay: false);
|
||||
}
|
||||
|
||||
public class EditLightningAddressVM
|
||||
@ -440,6 +437,13 @@ namespace BTCPayServer
|
||||
var store = await _storeRepository.FindStore(lightningAddressSettings.StoreDataId);
|
||||
if (store is null)
|
||||
return NotFound("Unknown username");
|
||||
List<string> additionalTags = new List<string>();
|
||||
if (blob?.PullPaymentId is not null)
|
||||
{
|
||||
var pp = await _pullPaymentHostedService.GetPullPayment(blob.PullPaymentId, false);
|
||||
if (pp != null)
|
||||
additionalTags.Add(PullPaymentHostedService.GetInternalTag(blob.PullPaymentId));
|
||||
}
|
||||
var result = await GetLNURLRequest(
|
||||
cryptoCode,
|
||||
store,
|
||||
@ -457,7 +461,7 @@ namespace BTCPayServer
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
{ "text/identifier", $"{username}@{Request.Host}" }
|
||||
});
|
||||
}, additionalTags);
|
||||
if (result is not OkObjectResult ok || ok.Value is not LNURLPayRequest payRequest)
|
||||
return result;
|
||||
var invoiceId = payRequest.Callback.AbsoluteUri.Split('/').Last();
|
||||
@ -499,7 +503,7 @@ namespace BTCPayServer
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<IActionResult> GetLNURLRequest(
|
||||
internal async Task<IActionResult> GetLNURLRequest(
|
||||
string cryptoCode,
|
||||
Data.StoreData store,
|
||||
Data.StoreBlob blob,
|
||||
@ -526,9 +530,7 @@ namespace BTCPayServer
|
||||
return this.CreateAPIError(null, e.Message);
|
||||
}
|
||||
lnurlRequest = await CreateLNUrlRequestFromInvoice(cryptoCode, i, store, blob, lnurlRequest, lnUrlMetadata, allowOverpay);
|
||||
return lnurlRequest is null
|
||||
? BadRequest(new LNUrlStatusResponse { Status = "ERROR", Reason = "Unable to create LNURL request." })
|
||||
: Ok(lnurlRequest);
|
||||
return lnurlRequest is null ? NotFound() : Ok(lnurlRequest);
|
||||
}
|
||||
|
||||
private async Task<LNURLPayRequest> CreateLNUrlRequestFromInvoice(
|
||||
|
@ -227,7 +227,6 @@ 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)
|
||||
@ -236,7 +235,6 @@ 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;
|
||||
@ -249,15 +247,12 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
paymentMethodId = supported.FirstOrDefault(id => vm.SelectedPaymentMethod == id.ToString());
|
||||
var payoutHandler = paymentMethodId is null ? null : _payoutHandlers.FindPayoutHandler(paymentMethodId);
|
||||
if (payoutHandler is not null)
|
||||
{
|
||||
(destination, error) = await payoutHandler.ParseAndValidateClaimDestination(paymentMethodId, vm.Destination, ppBlob, cancellationToken);
|
||||
}
|
||||
destination = payoutHandler is null ? null : (await payoutHandler.ParseAndValidateClaimDestination(paymentMethodId, vm.Destination, ppBlob, cancellationToken)).destination;
|
||||
}
|
||||
|
||||
if (destination is null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.Destination), error ?? "Invalid destination or payment method");
|
||||
ModelState.AddModelError(nameof(vm.Destination), "Invalid destination or payment method");
|
||||
return await ViewPullPayment(pullPaymentId);
|
||||
}
|
||||
|
||||
|
@ -64,27 +64,7 @@ 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");
|
||||
|
||||
@ -136,11 +116,14 @@ public partial class UIReportsController
|
||||
return Encoders.Hex.EncodeData(GenerateBytes(32));
|
||||
if (f.Name == "Rate")
|
||||
{
|
||||
return GetFormattedAmount();
|
||||
}
|
||||
if (f.Type == "amount")
|
||||
{
|
||||
return GetFormattedAmount();
|
||||
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 null;
|
||||
}
|
||||
|
@ -195,11 +195,10 @@ namespace BTCPayServer.Controllers
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
|
||||
ViewBag.UseCustomSMTP = useCustomSMTP;
|
||||
model.FallbackSettings = await _emailSenderFactory.GetEmailSender(store.Id) is StoreEmailSender { FallbackSender: not null } storeSender
|
||||
? await storeSender.FallbackSender.GetEmailSettings()
|
||||
: null;
|
||||
if (model.FallbackSettings is null) useCustomSMTP = true;
|
||||
ViewBag.UseCustomSMTP = useCustomSMTP;
|
||||
if (useCustomSMTP)
|
||||
{
|
||||
model.Settings.Validate("Settings.", ModelState);
|
||||
|
@ -151,12 +151,13 @@ namespace BTCPayServer.Controllers
|
||||
WalletId walletId, WalletPSBTViewModel vm, string command = null)
|
||||
{
|
||||
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode);
|
||||
var psbt = await vm.GetPSBT(network.NBitcoinNetwork, ModelState);
|
||||
var psbt = await vm.GetPSBT(network.NBitcoinNetwork);
|
||||
|
||||
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,
|
||||
@ -240,9 +241,10 @@ namespace BTCPayServer.Controllers
|
||||
vm.NBXSeedAvailable = await CanUseHotWallet() && derivationSchemeSettings.IsHotWallet;
|
||||
vm.BackUrl ??= HttpContext.Request.GetTypedHeaders().Referer?.AbsolutePath;
|
||||
|
||||
var psbt = await vm.GetPSBT(network.NBitcoinNetwork, ModelState);
|
||||
var psbt = await vm.GetPSBT(network.NBitcoinNetwork);
|
||||
if (vm.InvalidPSBT)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.PSBT), "Invalid PSBT");
|
||||
return View(vm);
|
||||
}
|
||||
if (psbt is null)
|
||||
@ -475,7 +477,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, ModelState);
|
||||
PSBT psbt = await vm.GetPSBT(network.NBitcoinNetwork);
|
||||
if (vm.InvalidPSBT || psbt is null)
|
||||
{
|
||||
if (vm.InvalidPSBT)
|
||||
@ -635,14 +637,16 @@ namespace BTCPayServer.Controllers
|
||||
WalletId walletId, WalletPSBTCombineViewModel vm)
|
||||
{
|
||||
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode);
|
||||
var psbt = await vm.GetPSBT(network.NBitcoinNetwork, ModelState);
|
||||
var psbt = await vm.GetPSBT(network.NBitcoinNetwork);
|
||||
if (psbt == null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.PSBT), "Invalid PSBT");
|
||||
return View(vm);
|
||||
}
|
||||
var sourcePSBT = vm.GetSourcePSBT(network.NBitcoinNetwork, ModelState);
|
||||
if (sourcePSBT is null)
|
||||
var sourcePSBT = vm.GetSourcePSBT(network.NBitcoinNetwork);
|
||||
if (sourcePSBT == 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);
|
||||
var walletObjectAddress = new WalletObjectId(walletId, WalletObjectData.Types.Address, transactionOutput.DestinationAddress.ToLowerInvariant());
|
||||
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.MilliSatoshi == LightMoney.Zero ? null: paymentRequest.MinimumAmount.ToDecimal(LightMoneyUnit.BTC);
|
||||
Amount = paymentRequest.MinimumAmount.ToDecimal(LightMoneyUnit.BTC);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
|
@ -73,11 +73,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike
|
||||
{
|
||||
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(30));
|
||||
using var t = CancellationTokenSource.CreateLinkedTokenSource(timeout.Token, cancellationToken);
|
||||
var rawInfo = await LNURL.LNURL.FetchInformation(lnurl, CreateClient(lnurl), t.Token);
|
||||
if(rawInfo is null)
|
||||
return (null, "The LNURL / Lightning Address provided was not online.");
|
||||
if(rawInfo is not LNURLPayRequest info)
|
||||
return (null, "The LNURL was not a valid LNURL Pay request.");
|
||||
var info = (LNURLPayRequest)(await LNURL.LNURL.FetchInformation(lnurl, CreateClient(lnurl), t.Token));
|
||||
lnurlTag = info.Tag;
|
||||
}
|
||||
|
||||
@ -147,27 +143,9 @@ namespace BTCPayServer.Data.Payouts.LightningLike
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task<decimal> GetMinimumPayoutAmount(PaymentMethodId paymentMethod, IClaimDestination claimDestination)
|
||||
public Task<decimal> GetMinimumPayoutAmount(PaymentMethodId paymentMethodId, IClaimDestination claimDestination)
|
||||
{
|
||||
if(claimDestination is LNURLPayClaimDestinaton lnurlPayClaimDestinaton)
|
||||
{
|
||||
try
|
||||
{
|
||||
var lnurl = lnurlPayClaimDestinaton.LNURL.IsValidEmail()
|
||||
? LNURL.LNURL.ExtractUriFromInternetIdentifier(lnurlPayClaimDestinaton.LNURL)
|
||||
: LNURL.LNURL.Parse(lnurlPayClaimDestinaton.LNURL, out var lnurlTag);
|
||||
|
||||
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(30));
|
||||
var rawInfo = await LNURL.LNURL.FetchInformation(lnurl, CreateClient(lnurl), timeout.Token);
|
||||
if (rawInfo is LNURLPayRequest info)
|
||||
return info.MinSendable.ToDecimal(LightMoneyUnit.BTC);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
return Money.Satoshis(1).ToDecimal(MoneyUnit.BTC);
|
||||
return Task.FromResult(Money.Satoshis(1).ToDecimal(MoneyUnit.BTC));
|
||||
}
|
||||
|
||||
public Dictionary<PayoutState, List<(string Action, string Text)>> GetPayoutSpecificActions()
|
||||
|
@ -244,15 +244,13 @@ namespace BTCPayServer.Data.Payouts.LightningLike
|
||||
var lm = new LightMoney(blob.CryptoAmount.Value, LightMoneyUnit.BTC);
|
||||
if (lm > lnurlInfo.MaxSendable || lm < lnurlInfo.MinSendable)
|
||||
{
|
||||
|
||||
payoutData.State = PayoutState.Cancelled;
|
||||
return (null, new ResultVM
|
||||
{
|
||||
PayoutId = payoutData.Id,
|
||||
Result = PayResult.Error,
|
||||
Destination = blob.Destination,
|
||||
Message =
|
||||
$"The LNURL provided would not generate an invoice of {lm.ToDecimal(LightMoneyUnit.Satoshi)} sats"
|
||||
$"The LNURL provided would not generate an invoice of {lm.MilliSatoshi}msats"
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,6 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using NBitcoin;
|
||||
using NBitcoin.Scripting;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
@ -101,7 +100,6 @@ namespace BTCPayServer
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(str);
|
||||
str = str.Trim();
|
||||
str = Regex.Replace(str, @"\s+", "");
|
||||
HashSet<string> hintedLabels = new HashSet<string>();
|
||||
if (!Network.Consensus.SupportSegwit)
|
||||
{
|
||||
|
@ -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)
|
||||
if ((string)context.RouteContext.RouteData.Values["appId"] is { } appId && !req.IsOnion())
|
||||
{
|
||||
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 && !req.IsOnion())
|
||||
if (!string.IsNullOrEmpty(redirectDomain) && req.Method != "POST" && !req.HasFormContentType)
|
||||
{
|
||||
var uri = new UriBuilder(req.Scheme, redirectDomain);
|
||||
if (req.Host.Port.HasValue)
|
||||
|
@ -109,6 +109,7 @@ namespace BTCPayServer.HostedServices
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Task<string> CreatePullPayment(string storeId, CreatePullPaymentRequest request)
|
||||
{
|
||||
return CreatePullPayment(new CreatePullPayment()
|
||||
@ -291,6 +292,8 @@ namespace BTCPayServer.HostedServices
|
||||
ArgumentNullException.ThrowIfNull(completionSource);
|
||||
Completion = completionSource;
|
||||
ClaimRequest = request;
|
||||
if (request.StoreId is null)
|
||||
throw new ArgumentNullException(nameof(request.StoreId));
|
||||
}
|
||||
|
||||
public TaskCompletionSource<ClaimRequest.ClaimResponse> Completion { get; set; }
|
||||
@ -341,11 +344,11 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
payoutHandler.StartBackgroundCheck(Subscribe);
|
||||
}
|
||||
_eventAggregator.Subscribe<Events.InvoiceEvent>(TopUpInvoiceCore);
|
||||
_eventAggregator.Subscribe<Events.InvoiceEvent>(TopUpInvoice);
|
||||
return new[] { Loop() };
|
||||
}
|
||||
|
||||
private void TopUpInvoiceCore(InvoiceEvent evt)
|
||||
private void TopUpInvoice(InvoiceEvent evt)
|
||||
{
|
||||
if (evt.EventCode == InvoiceEventCode.Completed || evt.EventCode == InvoiceEventCode.MarkedCompleted)
|
||||
{
|
||||
@ -355,7 +358,6 @@ namespace BTCPayServer.HostedServices
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Subscribe(params Type[] events)
|
||||
{
|
||||
foreach (Type @event in events)
|
||||
@ -372,7 +374,6 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
await HandleTopUp(topUp);
|
||||
}
|
||||
|
||||
if (o is PayoutRequest req)
|
||||
{
|
||||
await HandleCreatePayout(req);
|
||||
@ -410,7 +411,6 @@ 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()
|
||||
@ -423,11 +423,15 @@ namespace BTCPayServer.HostedServices
|
||||
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 rate = topUp.InvoiceEntity.Rates["BTC"];
|
||||
|
||||
var paidAmount = topUp.InvoiceEntity.PaidAmount.Net;
|
||||
if (paidAmount == 0.0m)
|
||||
// If marked as complete, the paid amount is not set.
|
||||
// HOWEVER THIS FIX ONLY WORK IF THE INVOICE IS IN BTC REMOVE THIS HACK AFTER CONFERENCE
|
||||
paidAmount = topUp.InvoiceEntity.Price;
|
||||
|
||||
var cryptoAmount = Math.Round(paidAmount / rate, 11);
|
||||
|
||||
var payoutBlob = new PayoutBlob()
|
||||
{
|
||||
@ -701,7 +705,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);
|
||||
|
@ -304,7 +304,6 @@ namespace BTCPayServer.Hosting
|
||||
});
|
||||
services.TryAddSingleton<BTCPayNetworkProvider>();
|
||||
|
||||
services.AddExceptionHandler<PluginExceptionHandler>();
|
||||
services.TryAddSingleton<AppService>();
|
||||
services.AddTransient<PluginService>();
|
||||
services.AddSingleton<PluginHookService>();
|
||||
@ -348,8 +347,6 @@ 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;
|
||||
});
|
||||
|
||||
@ -388,7 +385,6 @@ 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,7 +293,6 @@ namespace BTCPayServer.Hosting
|
||||
|
||||
app.UseStatusCodePagesWithReExecute("/errors/{0}");
|
||||
|
||||
app.UseExceptionHandler("/errors/{0}");
|
||||
app.UsePayServer();
|
||||
app.UseRouting();
|
||||
app.UseCors();
|
||||
|
@ -17,9 +17,8 @@ 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 RedirectUrl { get; set; }
|
||||
public string OrderUrl { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,6 @@ 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
|
||||
@ -18,7 +17,7 @@ namespace BTCPayServer.Models.WalletViewModels
|
||||
public string BackUrl { get; set; }
|
||||
public string ReturnUrl { get; set; }
|
||||
|
||||
public PSBT GetSourcePSBT(Network network, ModelStateDictionary modelState)
|
||||
public PSBT GetSourcePSBT(Network network)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(OtherPSBT))
|
||||
{
|
||||
@ -26,12 +25,12 @@ namespace BTCPayServer.Models.WalletViewModels
|
||||
{
|
||||
return NBitcoin.PSBT.Parse(OtherPSBT, network);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{ modelState.AddModelError(nameof(OtherPSBT), ex.Message); }
|
||||
catch
|
||||
{ }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
public async Task<PSBT> GetPSBT(Network network, ModelStateDictionary modelState)
|
||||
public async Task<PSBT> GetPSBT(Network network)
|
||||
{
|
||||
if (UploadedPSBTFile != null)
|
||||
{
|
||||
@ -46,9 +45,8 @@ namespace BTCPayServer.Models.WalletViewModels
|
||||
{
|
||||
return NBitcoin.PSBT.Load(bytes, network);
|
||||
}
|
||||
catch (FormatException ex)
|
||||
catch
|
||||
{
|
||||
modelState.AddModelError(nameof(UploadedPSBTFile), ex.Message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -58,10 +56,8 @@ namespace BTCPayServer.Models.WalletViewModels
|
||||
{
|
||||
return NBitcoin.PSBT.Parse(PSBT, network);
|
||||
}
|
||||
catch (FormatException ex)
|
||||
{
|
||||
modelState.AddModelError(nameof(UploadedPSBTFile), ex.Message);
|
||||
}
|
||||
catch
|
||||
{ }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
@ -4,7 +4,6 @@ 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
|
||||
@ -36,9 +35,9 @@ namespace BTCPayServer.Models.WalletViewModels
|
||||
public IFormFile UploadedPSBTFile { get; set; }
|
||||
|
||||
|
||||
public async Task<PSBT> GetPSBT(Network network, ModelStateDictionary modelState)
|
||||
public async Task<PSBT> GetPSBT(Network network)
|
||||
{
|
||||
var psbt = await GetPSBTCore(network, modelState);
|
||||
var psbt = await GetPSBTCore(network);
|
||||
if (psbt != null)
|
||||
{
|
||||
Decoded = psbt.ToString();
|
||||
@ -53,7 +52,7 @@ namespace BTCPayServer.Models.WalletViewModels
|
||||
}
|
||||
public bool InvalidPSBT { get; set; }
|
||||
|
||||
async Task<PSBT> GetPSBTCore(Network network, ModelStateDictionary modelState)
|
||||
async Task<PSBT> GetPSBTCore(Network network)
|
||||
{
|
||||
if (UploadedPSBTFile != null)
|
||||
{
|
||||
@ -69,20 +68,16 @@ namespace BTCPayServer.Models.WalletViewModels
|
||||
}
|
||||
return NBitcoin.PSBT.Load(bytes, network);
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch (Exception)
|
||||
{
|
||||
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))
|
||||
{
|
||||
@ -91,11 +86,8 @@ namespace BTCPayServer.Models.WalletViewModels
|
||||
InvalidPSBT = false;
|
||||
return NBitcoin.PSBT.Parse(PSBT, network);
|
||||
}
|
||||
catch (Exception ex) when (!InvalidPSBT)
|
||||
{
|
||||
modelState.AddModelError(nameof(PSBT), ex.Message);
|
||||
InvalidPSBT = true;
|
||||
}
|
||||
catch
|
||||
{ InvalidPSBT = true; }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
@ -1,10 +0,0 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Services.Altcoins.Monero.RPC.Models
|
||||
{
|
||||
public partial class OpenWalletErrorResponse
|
||||
{
|
||||
[JsonProperty("code")] public int Code { get; set; }
|
||||
[JsonProperty("message")] public string Message { get; set; }
|
||||
}
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Services.Altcoins.Monero.RPC.Models
|
||||
{
|
||||
public partial class OpenWalletRequest
|
||||
{
|
||||
[JsonProperty("filename")] public string Filename { get; set; }
|
||||
[JsonProperty("password")] public string Password { get; set; }
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Services.Altcoins.Monero.RPC.Models
|
||||
{
|
||||
public partial class OpenWalletResponse
|
||||
{
|
||||
[JsonProperty("id")] public string Id { get; set; }
|
||||
[JsonProperty("jsonrpc")] public string Jsonrpc { get; set; }
|
||||
[JsonProperty("result")] public object Result { get; set; }
|
||||
[JsonProperty("error")] public OpenWalletErrorResponse Error { get; set; }
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
#nullable enable
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Abstractions.Services;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Plugins.BoltcardFactory.Controllers;
|
||||
using BTCPayServer.Plugins.PointOfSale;
|
||||
using BTCPayServer.Plugins.PointOfSale.Controllers;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace BTCPayServer.Plugins.BoltcardBalance
|
||||
{
|
||||
public class BoltcardBalancePlugin : BaseBTCPayServerPlugin
|
||||
{
|
||||
public const string ViewsDirectory = "/Plugins/BoltcardBalance/Views";
|
||||
public const string AppType = "BoltcardBalance";
|
||||
|
||||
public override string Identifier => "BTCPayServer.Plugins.BoltcardBalance";
|
||||
public override string Name => "BoltcardBalance";
|
||||
public override string Description => "Add ability to check the history and balance of a Boltcard";
|
||||
|
||||
public override void Execute(IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<IUIExtension>(new UIExtension($"{ViewsDirectory}/NavExtension.cshtml", "header-nav"));
|
||||
base.Execute(services);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,143 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Reflection.Metadata;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Controllers.Greenfield;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.NTag424;
|
||||
using BTCPayServer.Plugins.BoltcardBalance.ViewModels;
|
||||
using BTCPayServer.Plugins.BoltcardFactory;
|
||||
using BTCPayServer.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NBitcoin.DataEncoders;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Plugins.BoltcardBalance.Controllers
|
||||
{
|
||||
[AutoValidateAntiforgeryToken]
|
||||
public class UIBoltcardBalanceController : Controller
|
||||
{
|
||||
private readonly ApplicationDbContextFactory _dbContextFactory;
|
||||
private readonly SettingsRepository _settingsRepository;
|
||||
private readonly BTCPayServerEnvironment _env;
|
||||
private readonly BTCPayNetworkJsonSerializerSettings _serializerSettings;
|
||||
|
||||
public UIBoltcardBalanceController(
|
||||
ApplicationDbContextFactory dbContextFactory,
|
||||
SettingsRepository settingsRepository,
|
||||
BTCPayServerEnvironment env,
|
||||
BTCPayNetworkJsonSerializerSettings serializerSettings)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_settingsRepository = settingsRepository;
|
||||
_env = env;
|
||||
_serializerSettings = serializerSettings;
|
||||
}
|
||||
[HttpGet("boltcards/balance")]
|
||||
public async Task<IActionResult> ScanCard([FromQuery] string p = null, [FromQuery] string c = null, [FromQuery]string view = null)
|
||||
{
|
||||
if (p is null || c is null)
|
||||
{
|
||||
return View($"{BoltcardBalancePlugin.ViewsDirectory}/ScanCard.cshtml");
|
||||
}
|
||||
|
||||
//return View($"{BoltcardBalancePlugin.ViewsDirectory}/BalanceView.cshtml", new BalanceViewModel()
|
||||
//{
|
||||
// AmountDue = 10000m,
|
||||
// Currency = "SATS",
|
||||
// Transactions = [new() { Date = DateTimeOffset.UtcNow, Balance = -3.0m }, new() { Date = DateTimeOffset.UtcNow, Balance = -5.0m }],
|
||||
// ViewMode = Mode.Reset
|
||||
//});
|
||||
|
||||
var issuerKey = await _settingsRepository.GetIssuerKey(_env);
|
||||
var boltData = issuerKey.TryDecrypt(p);
|
||||
if (boltData?.Uid is null)
|
||||
return NotFound();
|
||||
var id = issuerKey.GetId(boltData.Uid);
|
||||
var registration = await _dbContextFactory.GetBoltcardRegistration(issuerKey, boltData, true);
|
||||
if (registration is null)
|
||||
return NotFound();
|
||||
return await GetBalanceView(registration, p, issuerKey, view);
|
||||
}
|
||||
[NonAction]
|
||||
public async Task<IActionResult> GetBalanceView(BoltcardDataExtensions.BoltcardRegistration registration, string p, IssuerKey issuerKey, string view = null)
|
||||
{
|
||||
var ppId = registration.PullPaymentId;
|
||||
var boltCardKeys = issuerKey.CreatePullPaymentCardKey(registration.UId, registration.Version, ppId).DeriveBoltcardKeys(issuerKey);
|
||||
await using var ctx = _dbContextFactory.CreateContext();
|
||||
var pp = await ctx.PullPayments.FindAsync(ppId);
|
||||
if (pp is null)
|
||||
return NotFound();
|
||||
var blob = pp.GetBlob();
|
||||
|
||||
var payouts = (await ctx.Payouts.GetPayoutInPeriod(pp)
|
||||
.OrderByDescending(o => o.Date)
|
||||
.ToListAsync())
|
||||
.Select(o => new
|
||||
{
|
||||
Entity = o,
|
||||
Blob = o.GetBlob(_serializerSettings)
|
||||
});
|
||||
|
||||
|
||||
var totalPaid = payouts.Where(p => p.Entity.State != PayoutState.Cancelled).Select(p => p.Blob.Amount).Sum();
|
||||
|
||||
var bech32LNUrl = new Uri(Url.Action(nameof(UIBoltcardController.GetPayRequest), "UIBoltcard", new { p }, Request.Scheme), UriKind.Absolute);
|
||||
bech32LNUrl = LNURL.LNURL.EncodeUri(bech32LNUrl, "payRequest", true);
|
||||
var vm = new BalanceViewModel()
|
||||
{
|
||||
Currency = blob.Currency,
|
||||
AmountDue = blob.Limit - totalPaid,
|
||||
LNUrlBech32 = bech32LNUrl.AbsoluteUri,
|
||||
LNUrlPay = Url.Action(nameof(UIBoltcardController.GetPayRequest), "UIBoltcard", new { p }, "lnurlp"),
|
||||
BoltcardKeysResetLink = $"boltcard://reset?url={GetBoltcardDeeplinkUrl(pp.Id, OnExistingBehavior.KeepVersion)}",
|
||||
WipeData = JObject.FromObject(new
|
||||
{
|
||||
version = 1,
|
||||
action = "wipe",
|
||||
k0 = Encoders.Hex.EncodeData(boltCardKeys.AppMasterKey.ToBytes()).ToUpperInvariant(),
|
||||
k1 = Encoders.Hex.EncodeData(boltCardKeys.EncryptionKey.ToBytes()).ToUpperInvariant(),
|
||||
k2 = Encoders.Hex.EncodeData(boltCardKeys.AuthenticationKey.ToBytes()).ToUpperInvariant(),
|
||||
k3 = Encoders.Hex.EncodeData(boltCardKeys.K3.ToBytes()).ToUpperInvariant(),
|
||||
k4 = Encoders.Hex.EncodeData(boltCardKeys.K4.ToBytes()).ToUpperInvariant(),
|
||||
|
||||
}).ToString(Newtonsoft.Json.Formatting.None),
|
||||
PullPaymentLink = Url.Action(nameof(UIPullPaymentController.ViewPullPayment), "UIPullPayment", new { pullPaymentId = pp.Id }, Request.Scheme, Request.Host.ToString())
|
||||
};
|
||||
foreach (var payout in payouts)
|
||||
{
|
||||
vm.Transactions.Add(new BalanceViewModel.Transaction()
|
||||
{
|
||||
Date = payout.Entity.Date,
|
||||
Balance = -payout.Blob.Amount,
|
||||
Status = payout.Entity.State
|
||||
});
|
||||
}
|
||||
vm.Transactions.Add(new BalanceViewModel.Transaction()
|
||||
{
|
||||
Date = pp.StartDate,
|
||||
Balance = blob.Limit,
|
||||
Status = PayoutState.Completed
|
||||
});
|
||||
vm.ViewMode = view?.Equals("Reset", StringComparison.OrdinalIgnoreCase) is true ? Mode.Reset : Mode.TopUp;
|
||||
return View($"{BoltcardBalancePlugin.ViewsDirectory}/BalanceView.cshtml", vm);
|
||||
}
|
||||
|
||||
private string GetBoltcardDeeplinkUrl(string ppId, OnExistingBehavior onExisting)
|
||||
{
|
||||
var registerUrl = Url.Action(nameof(GreenfieldPullPaymentController.RegisterBoltcard), "GreenfieldPullPayment",
|
||||
new
|
||||
{
|
||||
pullPaymentId = ppId,
|
||||
onExisting = onExisting.ToString()
|
||||
}, Request.Scheme, Request.Host.ToString());
|
||||
registerUrl = Uri.EscapeDataString(registerUrl);
|
||||
return registerUrl;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Client.Models;
|
||||
|
||||
namespace BTCPayServer.Plugins.BoltcardBalance.ViewModels
|
||||
{
|
||||
public enum Mode
|
||||
{
|
||||
TopUp,
|
||||
Reset
|
||||
}
|
||||
public class BalanceViewModel
|
||||
{
|
||||
public class Transaction
|
||||
{
|
||||
public DateTimeOffset Date { get; set; }
|
||||
public bool Positive => Balance >= 0;
|
||||
public decimal Balance { get; set; }
|
||||
public PayoutState Status { get; internal set; }
|
||||
}
|
||||
public string Currency { get; set; }
|
||||
public decimal AmountDue { get; set; }
|
||||
public List<Transaction> Transactions { get; set; } = new List<Transaction>();
|
||||
public string LNUrlBech32 { get; set; }
|
||||
public string BoltcardKeysResetLink { get; set; }
|
||||
public string PullPaymentLink { get; set; }
|
||||
public string LNUrlPay { get; set; }
|
||||
|
||||
public string WipeData{ get; set; }
|
||||
public Mode ViewMode { get; set; }
|
||||
}
|
||||
}
|
111
BTCPayServer/Plugins/BoltcardBalance/Views/BalanceView.cshtml
Normal file
111
BTCPayServer/Plugins/BoltcardBalance/Views/BalanceView.cshtml
Normal file
@ -0,0 +1,111 @@
|
||||
@using BTCPayServer.Plugins.BoltcardBalance.ViewModels
|
||||
@using BTCPayServer.Services
|
||||
@inject DisplayFormatter DisplayFormatter
|
||||
|
||||
@model BalanceViewModel
|
||||
|
||||
@{
|
||||
Layout = null;
|
||||
}
|
||||
|
||||
<div class="col col-12 col-lg-12 mb-4">
|
||||
<div class="bg-tile h-100 m-0 p-3 p-sm-5 rounded">
|
||||
<nav id="wizard-navbar">
|
||||
@if (this.ViewData["NoCancelWizard"] is not true)
|
||||
{
|
||||
@if (Model.ViewMode == Mode.TopUp)
|
||||
{
|
||||
<button type="button" class="btn btn-secondary only-for-js mt-4" id="lnurlwithdraw-button">
|
||||
<span class="fa fa-qrcode fa-2x" title="Deposit"></span>
|
||||
</button>
|
||||
}
|
||||
<a href="#" id="CancelWizard" class="cancel mt-4">
|
||||
<vc:icon symbol="close" />
|
||||
</a>
|
||||
}
|
||||
</nav>
|
||||
<div class="d-flex justify-content-center">
|
||||
<div class="d-flex flex-column justify-content-center align-items-center">
|
||||
<dl class="mb-0 mt-md-4">
|
||||
<div class="d-flex d-print-inline-block flex-column mb-4">
|
||||
<dt class="h4 fw-semibold text-nowrap text-primary text-print-default order-2 order-sm-1 mb-1">@DisplayFormatter.Currency(Model.AmountDue, Model.Currency)</dt>
|
||||
</div>
|
||||
</dl>
|
||||
@if (Model.ViewMode == Mode.TopUp)
|
||||
{
|
||||
<div class="lnurl-pay boltcard-details d-none">
|
||||
<vc:qr-code data="@Model.LNUrlBech32" />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@if (Model.ViewMode == Mode.Reset)
|
||||
{
|
||||
@if (Model.AmountDue > 0)
|
||||
{
|
||||
<div class="d-flex justify-content-center">
|
||||
<a class="btn btn-outline-primary" href="@Model.PullPaymentLink">Sweep remaining balance</a>
|
||||
</div>
|
||||
}
|
||||
<div class="d-flex justify-content-center nfc-supported mt-2">
|
||||
<div class="boltcard-reset boltcard-details text-center">
|
||||
|
||||
<div class="d-flex justify-content-center">
|
||||
<div class="input-group">
|
||||
<a class="btn btn-outline-danger form-control" href="@Model.BoltcardKeysResetLink">
|
||||
<div style="margin-top:3px">Reset Boltcard</div>
|
||||
</a>
|
||||
<button type="button" class="btn btn-outline-danger input-group-btn" id="show-wipe-qr">
|
||||
<span class="fa fa-qrcode fa-2x" title="Show wipe QR"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="wipe-qr" class="d-none mt-2 cursor-pointer" data-clipboard-target="#qr-wipe-code-data-input">
|
||||
<div class="d-flex">
|
||||
<vc:qr-code data="@Model.WipeData" />
|
||||
</div>
|
||||
|
||||
<div class="d-flex">
|
||||
<div class="input-group input-group-sm mt-3">
|
||||
<input type="text" class="form-control" readonly value="@Model.WipeData" id="qr-wipe-code-data-input">
|
||||
<button type="button" class="btn btn-outline-secondary px-3" data-clipboard-target="#qr-wipe-code-data-input">
|
||||
<vc:icon symbol="copy" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-secondary mt-2">Requires installing the <a href="https://play.google.com/store/apps/details?id=com.lightningnfcapp&hl=en&gl=US">Bolt Card Creator app</a></p>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (Model.Transactions.Any())
|
||||
{
|
||||
<div class="col col-12 col-lg-12 mb-4">
|
||||
<div class="bg-tile h-100 m-0 p-3 p-sm-5 rounded table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="date-col">Date</th>
|
||||
<th class="amount-col">Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var tx in Model.Transactions)
|
||||
{
|
||||
<tr>
|
||||
<td class="date-col">@tx.Date.ToBrowserDate(ViewsRazor.DateDisplayFormat.Relative)</td>
|
||||
<td class="amount-col">
|
||||
<span data-sensitive class="text-@(tx.Positive ? "success" : "danger")">@DisplayFormatter.Currency(tx.Balance, Model.Currency, DisplayFormatter.CurrencyFormat.Code)</span>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
@using BTCPayServer.Client
|
||||
@using BTCPayServer.Plugins.BoltcardFactory
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@using BTCPayServer.Views.Apps
|
||||
@using BTCPayServer.Abstractions.Extensions
|
||||
@using BTCPayServer.Abstractions.TagHelpers
|
||||
|
||||
|
||||
<li class="nav-item">
|
||||
<a asp-area="" asp-controller="UIBoltcardBalance" asp-action="ScanCard" class="nav-link">
|
||||
<vc:icon symbol="pay-button" />
|
||||
<span>Boltcard Balance</span>
|
||||
</a>
|
||||
<a asp-area="" asp-controller="UIBoltcardBalance" asp-action="ScanCard" asp-route-view="Reset" class="nav-link">
|
||||
<vc:icon symbol="pay-button" />
|
||||
<span>Boltcard reset</span>
|
||||
</a>
|
||||
</li>
|
252
BTCPayServer/Plugins/BoltcardBalance/Views/ScanCard.cshtml
Normal file
252
BTCPayServer/Plugins/BoltcardBalance/Views/ScanCard.cshtml
Normal file
@ -0,0 +1,252 @@
|
||||
@{
|
||||
ViewData["Title"] = "Boltcard Balances";
|
||||
ViewData["ShowFooter"] = false;
|
||||
Layout = "/Views/Shared/_LayoutWizard.cshtml";
|
||||
}
|
||||
|
||||
@section PageHeadContent
|
||||
{
|
||||
<style>
|
||||
.amount-col {
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
}
|
||||
|
||||
<header class="text-center">
|
||||
<h1>Consult balance</h1>
|
||||
<p class="lead text-secondary mt-3" id="explanation">Scan your card for consulting the balance</p>
|
||||
</header>
|
||||
|
||||
<div id="body" class="my-4">
|
||||
<div id="error" class="d-flex align-items-center justify-content-center d-none">
|
||||
|
||||
<p class="text-danger"></p>
|
||||
</div>
|
||||
<div id="actions" class="d-flex align-items-center justify-content-center d-none">
|
||||
<div class="d-flex gap-3 mt-3 mt-sm-0">
|
||||
<a id="start-scan-btn" class="btn btn-primary" href="#">Ask permission...</a>
|
||||
</div>
|
||||
</div>
|
||||
<div id="qr" class="d-flex flex-column align-items-center justify-content-center d-none">
|
||||
<div class="d-inline-flex flex-column" style="width:256px">
|
||||
<div class="qr-container mb-2">
|
||||
<vc:qr-code data="@Context.Request.GetCurrentUrlWithQueryString()" />
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-secondary">NFC not supported in this device</p>
|
||||
<p class="text-secondary">Please use Chrome on Android, or a lightning wallet that supports Bolt cards, or extract the NFC value from the card (using an NFC reader app) and input below</p>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="nfc-manual-value" placeholder="lnurlw:..." />
|
||||
<button class="btn btn-secondary" id="nfc-manual-submit">Extract</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="scanning-btn" class="d-flex align-items-center justify-content-center d-none">
|
||||
<div class="d-flex gap-3 mt-3 mt-sm-0">
|
||||
<a id="scanning-btn-link" class="action-button" style="font-size: 50px;" ></a>
|
||||
</div>
|
||||
</div>
|
||||
<div id="balance" class="row">
|
||||
<div id="balance-table"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var permissionGranted = false;
|
||||
var ndef = null;
|
||||
var abortController = null;
|
||||
|
||||
function handleError(e){
|
||||
if (e) {
|
||||
document.querySelector("#error p").innerHTML = e.message;
|
||||
document.getElementById("error").classList.remove("d-none");
|
||||
}
|
||||
else
|
||||
{
|
||||
document.getElementById("error").classList.add("d-none");
|
||||
}
|
||||
}
|
||||
|
||||
function toggleDetailsWhenPressed(buttonId, className)
|
||||
{
|
||||
var button = document.getElementById(buttonId);
|
||||
if (button) {
|
||||
var el = document.getElementsByClassName("boltcard-details");
|
||||
button.addEventListener("click", function () {
|
||||
for (var i = 0; i < el.length; i++) {
|
||||
if (el[i].classList.contains(className)) {
|
||||
if (el[i].classList.contains("d-none"))
|
||||
el[i].classList.remove("d-none");
|
||||
else
|
||||
el[i].classList.add("d-none");
|
||||
}
|
||||
else {
|
||||
el[i].classList.add("d-none");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function delay(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
async function showBalance(lnurlw) {
|
||||
try {
|
||||
const initState = (!('NDEFReader' in window)) ? "NFCNotSupported" : "WaitingForCard";
|
||||
setState("Submitting");
|
||||
var uiDelay = delay(1000);
|
||||
var url = window.location.href.replace("#", "");
|
||||
url = url.split("?")[0] + "?" + lnurlw.split("?")[1];
|
||||
// url = "https://testnet.demo.btcpayserver.org/boltcards/balance?p=...&c=..."
|
||||
|
||||
var params = new URLSearchParams(window.location.search);
|
||||
if (params.toString()) {
|
||||
url += "&" + params;
|
||||
}
|
||||
|
||||
var xhttp = new XMLHttpRequest();
|
||||
xhttp.onreadystatechange = async function () {
|
||||
if (this.readyState == 4 && this.status == 200 && this.responseText) {
|
||||
document.getElementById("balance-table").innerHTML = this.responseText;
|
||||
document.getElementById("CancelWizard").addEventListener("click", function (e) {
|
||||
e.preventDefault();
|
||||
setState(initState);
|
||||
document.getElementById("balance-table").innerHTML = "";
|
||||
});
|
||||
|
||||
toggleDetailsWhenPressed('lnurlwithdraw-button', 'lnurl-pay');
|
||||
toggleDetailsWhenPressed('reset-button', 'boltcard-reset');
|
||||
|
||||
await uiDelay;
|
||||
setState("ShowBalance");
|
||||
}
|
||||
else if(this.readyState == 4 && this.status == 404) {
|
||||
setState(initState);
|
||||
handleError(new Error("Initialized by a different provider"));
|
||||
}
|
||||
else {
|
||||
setState(initState);
|
||||
}
|
||||
};
|
||||
xhttp.open('GET', url, true);
|
||||
xhttp.send(new FormData());
|
||||
}catch (e) {
|
||||
handleError(e);
|
||||
}
|
||||
}
|
||||
|
||||
async function startScan() {
|
||||
try {
|
||||
if (!('NDEFReader' in window)) {
|
||||
return;
|
||||
}
|
||||
ndef = new NDEFReader();
|
||||
abortController = new AbortController();
|
||||
abortController.signal.onabort = () => setState("WaitingForCard");
|
||||
|
||||
await ndef.scan({ signal: abortController.signal })
|
||||
setState("WaitingForCard");
|
||||
ndef.onreading = async ({ message }) => {
|
||||
handleError(null);
|
||||
const record = message.records[0];
|
||||
if (message.records.length === 0)
|
||||
{
|
||||
setState("WaitingForCard");
|
||||
handleError(new Error("Card is blank"));
|
||||
return;
|
||||
}
|
||||
const textDecoder = new TextDecoder('utf-8');
|
||||
const decoded = textDecoder.decode(record.data);
|
||||
await showBalance(decoded);
|
||||
};
|
||||
}
|
||||
catch (e) {
|
||||
handleError(e);
|
||||
}
|
||||
}
|
||||
|
||||
function setState(state)
|
||||
{
|
||||
document.querySelector("#error p").innerHTML = "";
|
||||
document.getElementById("error").classList.add("d-none");
|
||||
document.getElementById("actions").classList.add("d-none");
|
||||
document.getElementById("qr").classList.add("d-none");
|
||||
document.getElementById("scanning-btn").classList.add("d-none");
|
||||
document.getElementById("balance").classList.add("d-none");
|
||||
|
||||
if (state === "NFCNotSupported")
|
||||
{
|
||||
document.getElementById("qr").classList.remove("d-none");
|
||||
document.querySelectorAll(".nfc-supported").forEach(el => {
|
||||
el.classList.add("d-none");
|
||||
});
|
||||
}
|
||||
else if (state === "WaitingForPermission")
|
||||
{
|
||||
|
||||
document.getElementById("actions").classList.remove("d-none");
|
||||
}
|
||||
else if (state === "WaitingForCard")
|
||||
{
|
||||
document.getElementById("scanning-btn").classList.remove("d-none");
|
||||
document.getElementById("scanning-btn-link").innerHTML = "<i class=\"fa fa-wifi\"></i>";
|
||||
}
|
||||
else if (state == "Submitting")
|
||||
{
|
||||
document.getElementById("scanning-btn").classList.remove("d-none");
|
||||
document.getElementById("scanning-btn-link").innerHTML = "<i class=\"fa fa-spinner\"></i>"
|
||||
}
|
||||
else if (state == "ShowBalance") {
|
||||
document.getElementById("scanning-btn").classList.remove("d-none");
|
||||
document.getElementById("scanning-btn-link").innerHTML = "<i class=\"fa fa-wifi\"></i>";
|
||||
document.getElementById("balance").classList.remove("d-none");
|
||||
}
|
||||
}
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
try {
|
||||
|
||||
var nfcSupported = 'NDEFReader' in window;
|
||||
if (!nfcSupported) {
|
||||
setState("NFCNotSupported");
|
||||
}
|
||||
else {
|
||||
setState("WaitingForPermission");
|
||||
var granted = (await navigator.permissions.query({ name: 'nfc' })).state === 'granted';
|
||||
if (granted)
|
||||
{
|
||||
setState("WaitingForCard");
|
||||
startScan();
|
||||
}
|
||||
}
|
||||
delegate('click', "#start-scan-btn", startScan);
|
||||
delegate('click', "#nfc-manual-submit", async function () {
|
||||
var value = document.getElementById("nfc-manual-value").value;
|
||||
if (value) {
|
||||
await showBalance(value);
|
||||
document.querySelector(".boltcard-reset").classList.add("d-none");
|
||||
}
|
||||
});
|
||||
|
||||
delegate('click', "#show-wipe-qr", ()=>{
|
||||
const el = document.getElementById("wipe-qr");
|
||||
if (el.classList.contains("d-none")){
|
||||
el.classList.remove("d-none");
|
||||
} else {
|
||||
el.classList.add("d-none")
|
||||
}
|
||||
|
||||
});
|
||||
delegate('click', "#wipe-qr", ()=>{
|
||||
|
||||
});
|
||||
}
|
||||
catch (e) {
|
||||
handleError(e);
|
||||
}
|
||||
// showBalance("lnurl://ewfw?p=test&c=test");
|
||||
});
|
||||
})();
|
||||
</script>
|
@ -0,0 +1,74 @@
|
||||
#nullable enable
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Abstractions.Services;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Plugins.BoltcardFactory.Controllers;
|
||||
using BTCPayServer.Plugins.PointOfSale;
|
||||
using BTCPayServer.Plugins.PointOfSale.Controllers;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace BTCPayServer.Plugins.BoltcardFactory
|
||||
{
|
||||
public class BoltcardFactoryPlugin : BaseBTCPayServerPlugin
|
||||
{
|
||||
public const string ViewsDirectory = "/Plugins/BoltcardFactory/Views";
|
||||
public const string AppType = "BoltcardFactory";
|
||||
|
||||
public override string Identifier => "BTCPayServer.Plugins.BoltcardFactory";
|
||||
public override string Name => "BoltcardFactory";
|
||||
public override string Description => "Allow the creation of a consequential number of Boltcards in an efficient way";
|
||||
|
||||
|
||||
internal class BoltcardFactoryAppType : AppBaseType
|
||||
{
|
||||
private readonly LinkGenerator _linkGenerator;
|
||||
private readonly IOptions<BTCPayServerOptions> _btcPayServerOptions;
|
||||
|
||||
public BoltcardFactoryAppType(
|
||||
LinkGenerator linkGenerator,
|
||||
IOptions<BTCPayServerOptions> btcPayServerOptions)
|
||||
{
|
||||
Type = AppType;
|
||||
Description = "Boltcard Factories";
|
||||
_linkGenerator = linkGenerator;
|
||||
_btcPayServerOptions = btcPayServerOptions;
|
||||
}
|
||||
public override Task<string> ConfigureLink(AppData app)
|
||||
{
|
||||
return Task.FromResult(_linkGenerator.GetPathByAction(nameof(UIBoltcardFactoryController.UpdateBoltcardFactory),
|
||||
"UIBoltcardFactory", new { appId = app.Id }, _btcPayServerOptions.Value.RootPath)!);
|
||||
}
|
||||
|
||||
public override Task<object?> GetInfo(AppData appData)
|
||||
{
|
||||
return Task.FromResult<object?>(null);
|
||||
}
|
||||
|
||||
public override Task SetDefaultSettings(AppData appData, string defaultCurrency)
|
||||
{
|
||||
appData.SetSettings(new CreatePullPaymentRequest());
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public override Task<string> ViewLink(AppData app)
|
||||
{
|
||||
return Task.FromResult(_linkGenerator.GetPathByAction(nameof(UIBoltcardFactoryController.ViewBoltcardFactory),
|
||||
"UIBoltcardFactory", new { appId = app.Id }, _btcPayServerOptions.Value.RootPath)!);
|
||||
}
|
||||
}
|
||||
public override void Execute(IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<IUIExtension>(new UIExtension($"{ViewsDirectory}/NavExtension.cshtml", "header-nav"));
|
||||
services.AddSingleton<AppBaseType, BoltcardFactoryAppType>();
|
||||
base.Execute(services);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,150 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.NTag424;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin.DataEncoders;
|
||||
|
||||
namespace BTCPayServer.Plugins.BoltcardFactory.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Route("apps")]
|
||||
public class APIBoltcardFactoryController : ControllerBase
|
||||
{
|
||||
private readonly ILogger<APIBoltcardFactoryController> _logger;
|
||||
private readonly AppService _appService;
|
||||
private readonly SettingsRepository _settingsRepository;
|
||||
private readonly BTCPayServerEnvironment _env;
|
||||
private readonly ApplicationDbContextFactory _dbContextFactory;
|
||||
private readonly PullPaymentHostedService _ppService;
|
||||
|
||||
public APIBoltcardFactoryController(
|
||||
ILogger<APIBoltcardFactoryController> logger,
|
||||
AppService appService,
|
||||
SettingsRepository settingsRepository,
|
||||
BTCPayServerEnvironment env,
|
||||
ApplicationDbContextFactory dbContextFactory,
|
||||
PullPaymentHostedService ppService)
|
||||
{
|
||||
_logger = logger;
|
||||
_appService = appService;
|
||||
_settingsRepository = settingsRepository;
|
||||
_env = env;
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_ppService = ppService;
|
||||
}
|
||||
[HttpPost("{appId}/boltcards")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> RegisterBoltcard(string appId, RegisterBoltcardRequest? request, string? onExisting = null)
|
||||
{
|
||||
var app = await _appService.GetApp(appId, BoltcardFactoryPlugin.AppType);
|
||||
if (app is null)
|
||||
return NotFound();
|
||||
var issuerKey = await _settingsRepository.GetIssuerKey(_env);
|
||||
|
||||
// LNURLW is used by deeplinks
|
||||
if (request?.LNURLW is not null)
|
||||
{
|
||||
if (request.UID is not null)
|
||||
{
|
||||
_logger.LogInformation("You should pass either LNURLW or UID but not both");
|
||||
ModelState.AddModelError(nameof(request.LNURLW), "You should pass either LNURLW or UID but not both");
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
var p = ExtractP(request.LNURLW);
|
||||
if (p is null)
|
||||
{
|
||||
_logger.LogInformation("The LNURLW should contains a 'p=' parameter");
|
||||
ModelState.AddModelError(nameof(request.LNURLW), "The LNURLW should contains a 'p=' parameter");
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
if (issuerKey.TryDecrypt(p) is not BoltcardPICCData picc)
|
||||
{
|
||||
_logger.LogInformation("The LNURLW 'p=' parameter cannot be decrypted");
|
||||
ModelState.AddModelError(nameof(request.LNURLW), "The LNURLW 'p=' parameter cannot be decrypted");
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
request.UID = picc.Uid;
|
||||
}
|
||||
|
||||
if (request?.UID is null || request.UID.Length != 7)
|
||||
{
|
||||
_logger.LogInformation("The UID is required and should be 7 bytes");
|
||||
ModelState.AddModelError(nameof(request.UID), "The UID is required and should be 7 bytes");
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
|
||||
// Passing onExisting as a query parameter is used by deeplink
|
||||
request.OnExisting = onExisting switch
|
||||
{
|
||||
nameof(OnExistingBehavior.UpdateVersion) => OnExistingBehavior.UpdateVersion,
|
||||
nameof(OnExistingBehavior.KeepVersion) => OnExistingBehavior.KeepVersion,
|
||||
_ => request.OnExisting
|
||||
};
|
||||
|
||||
int version;
|
||||
string ppId;
|
||||
var registration = await _dbContextFactory.GetBoltcardRegistration(issuerKey, request.UID);
|
||||
|
||||
if (request.OnExisting == OnExistingBehavior.UpdateVersion)
|
||||
{
|
||||
var req = app.GetSettings<CreatePullPaymentRequest>();
|
||||
ppId = await _ppService.CreatePullPayment(app.StoreDataId, req);
|
||||
version = await _dbContextFactory.LinkBoltcardToPullPayment(ppId, issuerKey, request.UID, request.OnExisting);
|
||||
}
|
||||
// If it's a reset, do not create a new pull payment
|
||||
else
|
||||
{
|
||||
if (registration?.PullPaymentId is null)
|
||||
{
|
||||
_logger.LogInformation("This card isn't registered");
|
||||
ModelState.AddModelError(nameof(request.UID), "This card isn't registered");
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
ppId = registration.PullPaymentId;
|
||||
version = registration.Version;
|
||||
}
|
||||
|
||||
var keys = issuerKey.CreatePullPaymentCardKey(request.UID, version, ppId).DeriveBoltcardKeys(issuerKey);
|
||||
var boltcardUrl = Url.Action(nameof(UIBoltcardController.GetWithdrawRequest), "UIBoltcard");
|
||||
boltcardUrl = Request.GetAbsoluteUri(boltcardUrl);
|
||||
boltcardUrl = Regex.Replace(boltcardUrl, "^https?://", "lnurlw://");
|
||||
|
||||
var resp = new RegisterBoltcardResponse()
|
||||
{
|
||||
LNURLW = boltcardUrl,
|
||||
Version = version,
|
||||
K0 = Encoders.Hex.EncodeData(keys.AppMasterKey.ToBytes()).ToUpperInvariant(),
|
||||
K1 = Encoders.Hex.EncodeData(keys.EncryptionKey.ToBytes()).ToUpperInvariant(),
|
||||
K2 = Encoders.Hex.EncodeData(keys.AuthenticationKey.ToBytes()).ToUpperInvariant(),
|
||||
K3 = Encoders.Hex.EncodeData(keys.K3.ToBytes()).ToUpperInvariant(),
|
||||
K4 = Encoders.Hex.EncodeData(keys.K4.ToBytes()).ToUpperInvariant(),
|
||||
};
|
||||
|
||||
return Ok(resp);
|
||||
}
|
||||
private string? ExtractP(string? url)
|
||||
{
|
||||
if (url is null || !Uri.TryCreate(url, UriKind.Absolute, out var uri))
|
||||
return null;
|
||||
int num = uri.AbsoluteUri.IndexOf('?');
|
||||
if (num == -1)
|
||||
return null;
|
||||
string input = uri.AbsoluteUri.Substring(num);
|
||||
Match match = Regex.Match(input, "p=([a-f0-9A-F]{32})");
|
||||
if (!match.Success)
|
||||
return null;
|
||||
return match.Groups[1].Value;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,216 @@
|
||||
#nullable enable
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Models.WalletViewModels;
|
||||
using BTCPayServer.Plugins.PointOfSale;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using BTCPayServer.Plugins.PointOfSale.Models;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using System.Text.RegularExpressions;
|
||||
using System;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Payments;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using BTCPayServer.Client.Models;
|
||||
using Org.BouncyCastle.Ocsp;
|
||||
using BTCPayServer.NTag424;
|
||||
using NBitcoin.DataEncoders;
|
||||
using Newtonsoft.Json;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.HostedServices;
|
||||
using System.Threading;
|
||||
using BTCPayServer.Plugins.BoltcardFactory.ViewModels;
|
||||
using BTCPayServer.Controllers.Greenfield;
|
||||
using BTCPayServer.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace BTCPayServer.Plugins.BoltcardFactory.Controllers
|
||||
{
|
||||
[AutoValidateAntiforgeryToken]
|
||||
[Route("apps")]
|
||||
public class UIBoltcardFactoryController : Controller
|
||||
{
|
||||
private readonly IEnumerable<IPayoutHandler> _payoutHandlers;
|
||||
private readonly CurrencyNameTable _currencies;
|
||||
private readonly AppService _appService;
|
||||
private readonly StoreRepository _storeRepository;
|
||||
private readonly CurrencyNameTable _currencyNameTable;
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
|
||||
public UIBoltcardFactoryController(
|
||||
IEnumerable<IPayoutHandler> payoutHandlers,
|
||||
CurrencyNameTable currencies,
|
||||
AppService appService,
|
||||
StoreRepository storeRepository,
|
||||
CurrencyNameTable currencyNameTable,
|
||||
IAuthorizationService authorizationService)
|
||||
{
|
||||
_payoutHandlers = payoutHandlers;
|
||||
_currencies = currencies;
|
||||
_appService = appService;
|
||||
_storeRepository = storeRepository;
|
||||
_currencyNameTable = currencyNameTable;
|
||||
_authorizationService = authorizationService;
|
||||
}
|
||||
public Data.StoreData CurrentStore => HttpContext.GetStoreData();
|
||||
private AppData GetCurrentApp() => HttpContext.GetAppData();
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[HttpGet("{appId}/settings/boltcardfactory")]
|
||||
public async Task<IActionResult> UpdateBoltcardFactory(string appId)
|
||||
{
|
||||
if (CurrentStore is null || GetCurrentApp() is null)
|
||||
return NotFound();
|
||||
|
||||
var paymentMethods = await _payoutHandlers.GetSupportedPaymentMethods(CurrentStore);
|
||||
if (!paymentMethods.Any())
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Message = "You must enable at least one payment method before creating a pull payment.",
|
||||
Severity = StatusMessageModel.StatusSeverity.Error
|
||||
});
|
||||
return RedirectToAction(nameof(UIStoresController.Dashboard), "UIStores", new { storeId = CurrentStore.Id });
|
||||
}
|
||||
|
||||
var req = GetCurrentApp().GetSettings<CreatePullPaymentRequest>();
|
||||
return base.View($"{BoltcardFactoryPlugin.ViewsDirectory}/UpdateBoltcardFactory.cshtml", CreateViewModel(paymentMethods, req));
|
||||
}
|
||||
|
||||
private static NewPullPaymentModel CreateViewModel(List<PaymentMethodId> paymentMethods, CreatePullPaymentRequest req)
|
||||
{
|
||||
return new NewPullPaymentModel
|
||||
{
|
||||
Name = req.Name,
|
||||
Currency = req.Currency,
|
||||
Amount = req.Amount,
|
||||
AutoApproveClaims = req.AutoApproveClaims,
|
||||
Description = req.Description,
|
||||
PaymentMethods = req.PaymentMethods,
|
||||
BOLT11Expiration = req.BOLT11Expiration?.TotalDays is double v ? (long)v : 30,
|
||||
PaymentMethodItems =
|
||||
paymentMethods.Select(id => new SelectListItem(id.ToPrettyString(), id.ToString(), true))
|
||||
};
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[HttpPost("{appId}/settings/boltcardfactory")]
|
||||
public async Task<IActionResult> UpdateBoltcardFactory(string appId, NewPullPaymentModel model)
|
||||
{
|
||||
if (CurrentStore is null)
|
||||
return NotFound();
|
||||
var storeId = CurrentStore.Id;
|
||||
var paymentMethodOptions = await _payoutHandlers.GetSupportedPaymentMethods(CurrentStore);
|
||||
model.PaymentMethodItems =
|
||||
paymentMethodOptions.Select(id => new SelectListItem(id.ToPrettyString(), id.ToString(), true));
|
||||
model.Name ??= string.Empty;
|
||||
model.Currency = model.Currency?.ToUpperInvariant()?.Trim() ?? String.Empty;
|
||||
model.PaymentMethods ??= new List<string>();
|
||||
|
||||
if (!model.PaymentMethods.Any())
|
||||
{
|
||||
// Since we assign all payment methods to be selected by default above we need to update
|
||||
// them here to reflect user's selection so that they can correct their mistake
|
||||
model.PaymentMethodItems =
|
||||
paymentMethodOptions.Select(id => new SelectListItem(id.ToPrettyString(), id.ToString(), false));
|
||||
ModelState.AddModelError(nameof(model.PaymentMethods), "You need at least one payment method");
|
||||
}
|
||||
if (_currencyNameTable.GetCurrencyData(model.Currency, false) is null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.Currency), "Invalid currency");
|
||||
}
|
||||
if (model.Amount <= 0.0m)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.Amount), "The amount should be more than zero");
|
||||
}
|
||||
if (model.Name.Length > 50)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.Name), "The name should be maximum 50 characters.");
|
||||
}
|
||||
|
||||
var selectedPaymentMethodIds = model.PaymentMethods.Select(PaymentMethodId.Parse).ToArray();
|
||||
if (!selectedPaymentMethodIds.All(id => selectedPaymentMethodIds.Contains(id)))
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.Name), "Not all payment methods are supported");
|
||||
}
|
||||
if (!ModelState.IsValid)
|
||||
return View(model);
|
||||
model.AutoApproveClaims = model.AutoApproveClaims && (await
|
||||
_authorizationService.AuthorizeAsync(User, CurrentStore.Id, Policies.CanCreatePullPayments)).Succeeded;
|
||||
|
||||
var req = new CreatePullPaymentRequest()
|
||||
{
|
||||
Name = model.Name,
|
||||
Description = model.Description,
|
||||
Currency = model.Currency,
|
||||
Amount = model.Amount,
|
||||
AutoApproveClaims = model.AutoApproveClaims,
|
||||
BOLT11Expiration = TimeSpan.FromDays(model.BOLT11Expiration),
|
||||
PaymentMethods = model.PaymentMethods.ToArray()
|
||||
};
|
||||
var app = GetCurrentApp();
|
||||
app.SetSettings(req);
|
||||
await _appService.UpdateOrCreateApp(app);
|
||||
var paymentMethods = await _payoutHandlers.GetSupportedPaymentMethods(CurrentStore);
|
||||
this.TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
{
|
||||
Message = "Pull payment request created",
|
||||
Severity = StatusMessageModel.StatusSeverity.Success
|
||||
});
|
||||
return View($"{BoltcardFactoryPlugin.ViewsDirectory}/UpdateBoltcardFactory.cshtml", CreateViewModel(paymentMethods, req));
|
||||
}
|
||||
private async Task<string?> GetStoreDefaultCurrentIfEmpty(string storeId, string? currency)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(currency))
|
||||
{
|
||||
currency = (await _storeRepository.FindStore(storeId))?.GetStoreBlob()?.DefaultCurrency;
|
||||
}
|
||||
return currency?.Trim().ToUpperInvariant();
|
||||
}
|
||||
private int[] ListSplit(string list, string separator = ",")
|
||||
{
|
||||
if (string.IsNullOrEmpty(list))
|
||||
{
|
||||
return Array.Empty<int>();
|
||||
}
|
||||
|
||||
// Remove all characters except numeric and comma
|
||||
Regex charsToDestroy = new Regex(@"[^\d|\" + separator + "]");
|
||||
list = charsToDestroy.Replace(list, "");
|
||||
|
||||
return list.Split(separator, StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToArray();
|
||||
}
|
||||
[HttpGet("/apps/{appId}/boltcardfactory")]
|
||||
[DomainMappingConstraint(BoltcardFactoryPlugin.AppType)]
|
||||
[AllowAnonymous]
|
||||
public IActionResult ViewBoltcardFactory(string appId)
|
||||
{
|
||||
var vm = new ViewBoltcardFactoryViewModel();
|
||||
vm.SetupDeepLink = $"boltcard://program?url={GetBoltcardDeeplinkUrl(appId, OnExistingBehavior.UpdateVersion)}";
|
||||
vm.ResetDeepLink = $"boltcard://reset?url={GetBoltcardDeeplinkUrl(appId, OnExistingBehavior.KeepVersion)}";
|
||||
return View($"{BoltcardFactoryPlugin.ViewsDirectory}/ViewBoltcardFactory.cshtml", vm);
|
||||
}
|
||||
|
||||
private string GetBoltcardDeeplinkUrl(string appId, OnExistingBehavior onExisting)
|
||||
{
|
||||
var registerUrl = Url.Action(nameof(APIBoltcardFactoryController.RegisterBoltcard), "APIBoltcardFactory",
|
||||
new
|
||||
{
|
||||
appId = appId,
|
||||
onExisting = onExisting.ToString()
|
||||
}, Request.Scheme, Request.Host.ToString());
|
||||
registerUrl = Uri.EscapeDataString(registerUrl!);
|
||||
return registerUrl;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
namespace BTCPayServer.Plugins.BoltcardFactory.ViewModels;
|
||||
|
||||
public class ViewBoltcardFactoryViewModel
|
||||
{
|
||||
public string SetupDeepLink { get; set; }
|
||||
public string ResetDeepLink { get; set; }
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
@using BTCPayServer.Client
|
||||
@using BTCPayServer.Plugins.BoltcardFactory
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@using BTCPayServer.Views.Apps
|
||||
@using BTCPayServer.Abstractions.Extensions
|
||||
@using BTCPayServer.Abstractions.TagHelpers
|
||||
@using BTCPayServer.Plugins.PointOfSale
|
||||
@using BTCPayServer.Services.Apps
|
||||
@inject AppService AppService;
|
||||
@model BTCPayServer.Components.MainNav.MainNavViewModel
|
||||
@{
|
||||
var store = Context.GetStoreData();
|
||||
}
|
||||
|
||||
@if (store != null)
|
||||
{
|
||||
var appType = AppService.GetAppType(BoltcardFactoryPlugin.AppType)!;
|
||||
<li class="nav-item" permission="@Policies.CanModifyStoreSettings">
|
||||
<a asp-area="" asp-controller="UIApps" asp-action="CreateApp" asp-route-storeId="@store.Id" asp-route-appType="@appType.Type" class="nav-link @ViewData.IsActivePage(AppsNavPages.Create, appType.Type)" id="@($"StoreNav-Create{appType.Type}")">
|
||||
<vc:icon symbol="pointofsale" />
|
||||
<span>@appType.Description</span>
|
||||
</a>
|
||||
</li>
|
||||
@foreach (var app in Model.Apps.Where(app => app.AppType == appType.Type))
|
||||
{
|
||||
<li class="nav-item nav-item-sub" permission="@Policies.CanModifyStoreSettings">
|
||||
<a asp-area="" asp-controller="UIBoltcardFactory" asp-action="UpdateBoltcardFactory" asp-route-appId="@app.Id" class="nav-link @ViewData.IsActivePage(AppsNavPages.Update, app.Id)" id="@($"StoreNav-App-{app.Id}")">
|
||||
<span>@app.AppName</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item nav-item-sub" not-permission="@Policies.CanModifyStoreSettings">
|
||||
<a asp-area="" asp-controller="UIBoltcardFactory" asp-action="ViewBoltcardFactory" asp-route-appId="@app.Id" class="nav-link">
|
||||
<span>@app.AppName</span>
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
}
|
@ -0,0 +1,130 @@
|
||||
@using BTCPayServer.Abstractions.Extensions
|
||||
@using BTCPayServer.Client
|
||||
@using BTCPayServer.Views.Stores
|
||||
@model BTCPayServer.Models.WalletViewModels.NewPullPaymentModel
|
||||
@{
|
||||
ViewData["Title"] = "Update Boltcard Factory";
|
||||
Layout = "/Views/Shared/_Layout.cshtml";
|
||||
}
|
||||
|
||||
@section PageHeadContent {
|
||||
<link href="~/vendor/summernote/summernote-bs5.css" rel="stylesheet" asp-append-version="true"/>
|
||||
}
|
||||
|
||||
@section PageFootContent {
|
||||
<partial name="_ValidationScriptsPartial"/>
|
||||
<script src="~/vendor/summernote/summernote-bs5.js" asp-append-version="true"></script>
|
||||
}
|
||||
|
||||
<form method="post" asp-route-appId="@Context.GetRouteValue("appId")" asp-action="UpdateBoltcardFactory">
|
||||
<div class="sticky-header d-flex align-items-center justify-content-between">
|
||||
<h2 class="mb-0">@ViewData["Title"]</h2>
|
||||
<div class="d-flex gap-3 mt-3 mt-sm-0">
|
||||
<a class="btn btn-secondary" asp-controller="UIBoltcardFactory" asp-action="ViewBoltcardFactory" asp-route-appId="@Context.GetRouteValue("appId")" id="ViewApp" target="_blank">View</a>
|
||||
<input type="submit" value="Save" class="btn btn-primary order-sm-1" id="Save" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<partial name="_StatusMessage"/>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label asp-for="Name" class="form-label"></label>
|
||||
<input asp-for="Name" class="form-control"/>
|
||||
<span asp-validation-for="Name" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="form-group col-8">
|
||||
<label asp-for="Amount" class="form-label" data-required></label>
|
||||
<input asp-for="Amount" class="form-control" inputmode="decimal"/>
|
||||
<span asp-validation-for="Amount" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group col-4">
|
||||
<label asp-for="Currency" class="form-label"></label>
|
||||
<input asp-for="Currency" class="form-control w-auto" currency-selection />
|
||||
<span asp-validation-for="Currency" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div class="form-group col-12" permission="@Policies.CanCreatePullPayments">
|
||||
<div class="form-check ">
|
||||
<input asp-for="AutoApproveClaims" type="checkbox" class="form-check-input"/>
|
||||
<label asp-for="AutoApproveClaims" class="form-check-label"></label>
|
||||
<span asp-validation-for="AutoApproveClaims" class="text-danger"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group mb-4">
|
||||
<label asp-for="PaymentMethods" class="form-label"></label>
|
||||
@foreach (var item in Model.PaymentMethodItems)
|
||||
{
|
||||
<div class="form-check mb-2">
|
||||
<label class="form-label">
|
||||
<input name="PaymentMethods" class="form-check-input" type="checkbox" value="@item.Value" @(item.Selected ? "checked" : "")>
|
||||
@item.Text
|
||||
</label>
|
||||
</div>
|
||||
}
|
||||
<span asp-validation-for="PaymentMethods" class="text-danger mt-0"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-9">
|
||||
<div class="form-group mb-4">
|
||||
<label asp-for="Description" class="form-label"></label>
|
||||
<textarea asp-for="Description" class="form-control richtext"></textarea>
|
||||
<span asp-validation-for="Description" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<h4 class="mt-5 mb-2">Additional Options</h4>
|
||||
<div class="form-group">
|
||||
<div class="accordion" id="additional">
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="additional-custom-css-header">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#additional-custom-css" aria-expanded="false" aria-controls="additional-custom-css">
|
||||
Custom CSS
|
||||
<vc:icon symbol="caret-down"/>
|
||||
</button>
|
||||
</h2>
|
||||
<div id="additional-custom-css" class="accordion-collapse collapse" aria-labelledby="additional-custom-css-header">
|
||||
<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...">
|
||||
<vc:icon symbol="info" />
|
||||
</a>
|
||||
<input asp-for="CustomCSSLink" class="form-control"/>
|
||||
<span asp-validation-for="CustomCSSLink" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="EmbeddedCSS" class="form-label"></label>
|
||||
<textarea asp-for="EmbeddedCSS" rows="10" cols="40" class="form-control"></textarea>
|
||||
<span asp-validation-for="EmbeddedCSS" class="text-danger"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="additional-lightning-header">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#additional-lightning" aria-expanded="false" aria-controls="additional-lightning">
|
||||
Lightning network settings
|
||||
<vc:icon symbol="caret-down"/>
|
||||
</button>
|
||||
</h2>
|
||||
<div id="additional-lightning" class="accordion-collapse collapse" aria-labelledby="additional-lightning-header">
|
||||
<div class="accordion-body">
|
||||
<div class="form-group">
|
||||
<label asp-for="BOLT11Expiration" class="form-label"></label>
|
||||
<div class="input-group">
|
||||
<input inputmode="numeric" asp-for="BOLT11Expiration" class="form-control" style="max-width:12ch;"/>
|
||||
<span class="input-group-text">days</span>
|
||||
</div>
|
||||
<span asp-validation-for="BOLT11Expiration" class="text-danger"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
@ -0,0 +1,44 @@
|
||||
@model ViewBoltcardFactoryViewModel
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Boltcard factory";
|
||||
Layout = "/Views/Shared/_LayoutWizard.cshtml";
|
||||
}
|
||||
|
||||
<header class="text-center">
|
||||
<h1>Program Boltcards</h1>
|
||||
<p class="lead text-secondary mt-3" id="explanation">Using Boltcard NFC Programmer</p>
|
||||
</header>
|
||||
|
||||
<div id="body" class="my-4">
|
||||
<div id="actions" class="d-flex align-items-center justify-content-center" style="visibility:hidden">
|
||||
<div class="d-flex gap-3 mt-3 mt-sm-0">
|
||||
<a class="btn btn-primary" href="@Model.SetupDeepLink" target="_blank">Setup</a>
|
||||
<a class="btn btn-danger" href="@Model.ResetDeepLink" target="_blank">Reset</a>
|
||||
</div>
|
||||
</div>
|
||||
<div id="qr" class="d-flex align-items-center justify-content-center">
|
||||
<div class="d-inline-flex flex-column" style="width:256px" style="visibility:hidden">
|
||||
<div class="qr-container mb-2">
|
||||
<vc:qr-code data="@Context.Request.GetCurrentUrl()" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
var isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
||||
if (isMobile) {
|
||||
document.getElementById("actions").style.visibility = "visible";
|
||||
document.getElementById("qr").style.visibility = "hidden";
|
||||
}
|
||||
else {
|
||||
document.getElementById("actions").style.visibility = "hidden";
|
||||
document.getElementById("qr").style.visibility = "visible";
|
||||
document.getElementById("explanation").innerText = "Scan the QR code with your mobile device";
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -0,0 +1 @@
|
||||
@using BTCPayServer.Plugins.BoltcardFactory.ViewModels;
|
@ -13,6 +13,7 @@ namespace BTCPayServer.Plugins.PayButton.Models
|
||||
[ModelBinder(BinderType = typeof(InvariantDecimalModelBinder))]
|
||||
public decimal? Price { get; set; }
|
||||
public string InvoiceId { get; set; }
|
||||
[Required]
|
||||
public string Currency { get; set; }
|
||||
public string DefaultPaymentMethod { get; set; }
|
||||
public PaymentMethodOptionViewModel.Format[] PaymentMethods { get; set; }
|
||||
|
@ -64,17 +64,5 @@ namespace BTCPayServer.Plugins
|
||||
var result = await httpClient.GetStringAsync($"api/v1/plugins{queryString}");
|
||||
return JsonConvert.DeserializeObject<PublishedVersion[]>(result, serializerSettings) ?? throw new InvalidOperationException();
|
||||
}
|
||||
public async Task<PublishedVersion> GetPlugin(string pluginSlug, string version)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await httpClient.GetStringAsync($"api/v1/plugins/{pluginSlug}/versions/{version}");
|
||||
return JsonConvert.DeserializeObject<PublishedVersion>(result, serializerSettings);
|
||||
}
|
||||
catch (HttpRequestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,53 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Logging;
|
||||
using Microsoft.AspNetCore.Diagnostics;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Hosting.Internal;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace BTCPayServer.Plugins
|
||||
{
|
||||
public class PluginExceptionHandler : IExceptionHandler
|
||||
{
|
||||
readonly string _pluginDir;
|
||||
readonly IHostApplicationLifetime _applicationLifetime;
|
||||
private readonly Logs _logs;
|
||||
|
||||
public PluginExceptionHandler(IOptions<DataDirectories> options, IHostApplicationLifetime applicationLifetime, Logs logs)
|
||||
{
|
||||
_applicationLifetime = applicationLifetime;
|
||||
_logs = logs;
|
||||
_pluginDir = options.Value.PluginDir;
|
||||
}
|
||||
public ValueTask<bool> TryHandleAsync(HttpContext httpContext, Exception exception, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!GetDisablePluginIfCrash(httpContext) ||
|
||||
!PluginManager.IsExceptionByPlugin(exception, out var pluginName))
|
||||
return ValueTask.FromResult(false);
|
||||
_logs.Configuration.LogError(exception, $"Unhandled exception caused by plugin '{pluginName}', disabling it and restarting...");
|
||||
PluginManager.DisablePlugin(_pluginDir, pluginName);
|
||||
_ = Task.Delay(3000).ContinueWith((t) => _applicationLifetime.StopApplication());
|
||||
// Returning true here means we will see Error 500 error message.
|
||||
// Returning false means that the user will see a stacktrace.
|
||||
return ValueTask.FromResult(false);
|
||||
}
|
||||
|
||||
public static bool GetDisablePluginIfCrash(HttpContext httpContext)
|
||||
{
|
||||
return httpContext.Items.TryGetValue("DisablePluginIfCrash", out object renderingDashboard) ||
|
||||
renderingDashboard is not true;
|
||||
}
|
||||
public static void SetDisablePluginIfCrash(HttpContext httpContext)
|
||||
{
|
||||
httpContext.Items.TryAdd("DisablePluginIfCrash", true);
|
||||
}
|
||||
}
|
||||
}
|
@ -258,27 +258,12 @@ namespace BTCPayServer.Plugins
|
||||
|
||||
private static IEnumerable<IBTCPayServerPlugin> GetPluginInstancesFromAssembly(Assembly assembly)
|
||||
{
|
||||
return GetTypesNotCrash(assembly).Where(type =>
|
||||
return assembly.GetTypes().Where(type =>
|
||||
typeof(IBTCPayServerPlugin).IsAssignableFrom(type) && type != typeof(PluginService.AvailablePlugin) &&
|
||||
!type.IsAbstract).
|
||||
Select(type => (IBTCPayServerPlugin)Activator.CreateInstance(type, Array.Empty<object>()));
|
||||
}
|
||||
|
||||
private static IEnumerable<Type> GetTypesNotCrash(Assembly assembly)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Strange crash with selenium
|
||||
if (assembly.FullName.Contains("Selenium", StringComparison.OrdinalIgnoreCase))
|
||||
return Array.Empty<Type>();
|
||||
return assembly.GetTypes();
|
||||
}
|
||||
catch(ReflectionTypeLoadException ex)
|
||||
{
|
||||
return ex.Types.Where(t => t is not null).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
private static IBTCPayServerPlugin GetPluginInstanceFromAssembly(string pluginIdentifier, Assembly assembly)
|
||||
{
|
||||
return GetPluginInstancesFromAssembly(assembly).FirstOrDefault(plugin => plugin.Identifier == pluginIdentifier);
|
||||
|
@ -70,11 +70,9 @@ namespace BTCPayServer.Plugins
|
||||
var dest = _dataDirectories.Value.PluginDir;
|
||||
var filedest = Path.Join(dest, pluginIdentifier + ".btcpay");
|
||||
var filemanifestdest = Path.Join(dest, pluginIdentifier + ".json");
|
||||
var pluginSelector = $"[{Uri.EscapeDataString(pluginIdentifier)}]";
|
||||
version = Uri.EscapeDataString(version);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(filedest));
|
||||
var url = $"api/v1/plugins/{pluginSelector}/versions/{version}/download";
|
||||
var manifest = (await _pluginBuilderClient.GetPlugin(pluginSelector, version))?.ManifestInfo?.ToObject<AvailablePlugin>();
|
||||
var url = $"api/v1/plugins/[{Uri.EscapeDataString(pluginIdentifier)}]/versions/{Uri.EscapeDataString(version)}/download";
|
||||
var manifest = (await _pluginBuilderClient.GetPublishedVersions(null, true)).Select(v => v.ManifestInfo.ToObject<AvailablePlugin>()).FirstOrDefault(p => p.Identifier == pluginIdentifier);
|
||||
await File.WriteAllTextAsync(filemanifestdest, JsonConvert.SerializeObject(manifest, Formatting.Indented));
|
||||
using var resp2 = await _pluginBuilderClient.HttpClient.GetAsync(url);
|
||||
await using var fs = new FileStream(filedest, FileMode.Create, FileAccess.ReadWrite);
|
||||
|
@ -356,9 +356,11 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
var receiptData = new JObject();
|
||||
if (choice is not null)
|
||||
{
|
||||
var dict = new Dictionary<string, string> { { "Title", choice.Title } };
|
||||
if (!string.IsNullOrEmpty(choice.Description)) dict["Description"] = choice.Description;
|
||||
receiptData = JObject.FromObject(dict);
|
||||
receiptData = JObject.FromObject(new Dictionary<string, string>
|
||||
{
|
||||
{"Title", choice.Title},
|
||||
{"Description", choice.Description},
|
||||
});
|
||||
}
|
||||
else if (jposData is not null)
|
||||
{
|
||||
|
@ -0,0 +1,87 @@
|
||||
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 apiVersion = "2020-07")
|
||||
string relativeUrl = null)
|
||||
{
|
||||
var url =
|
||||
$"https://{(shopName.Contains('.', StringComparison.InvariantCulture) ? shopName : $"{shopName}.myshopify.com")}/{relativeUrl ?? ($"admin/api/{apiVersion}/" + action)}";
|
||||
$"https://{(shopName.Contains('.', StringComparison.InvariantCulture) ? shopName : $"{shopName}.myshopify.com")}/{relativeUrl ?? ("admin/api/2020-07/" + action)}";
|
||||
var req = new HttpRequestMessage(method, url);
|
||||
return req;
|
||||
}
|
||||
@ -115,15 +115,6 @@ 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()
|
||||
{
|
||||
|
116
BTCPayServer/Plugins/Shopify/ShopifyOrderMarkerHostedService.cs
Normal file
116
BTCPayServer/Plugins/Shopify/ShopifyOrderMarkerHostedService.cs
Normal file
@ -0,0 +1,116 @@
|
||||
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,8 +14,7 @@ namespace BTCPayServer.Plugins.Shopify
|
||||
|
||||
public override void Execute(IServiceCollection applicationBuilder)
|
||||
{
|
||||
applicationBuilder.AddSingleton<ShopifyService>();
|
||||
applicationBuilder.AddSingleton<IHostedService, ShopifyService>(provider => provider.GetRequiredService<ShopifyService>());
|
||||
applicationBuilder.AddSingleton<IHostedService, ShopifyOrderMarkerHostedService>();
|
||||
applicationBuilder.AddSingleton<IUIExtension>(new UIExtension("Shopify/NavExtension", "header-nav"));
|
||||
base.Execute(applicationBuilder);
|
||||
}
|
||||
|
@ -1,234 +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.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,7 +34,6 @@ 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;
|
||||
@ -44,9 +43,7 @@ namespace BTCPayServer.Plugins.Shopify
|
||||
private readonly IJsonHelper _jsonHelper;
|
||||
private readonly IHttpClientFactory _clientFactory;
|
||||
|
||||
public UIShopifyController(
|
||||
ShopifyService shopifyService,
|
||||
BTCPayServerEnvironment btcPayServerEnvironment,
|
||||
public UIShopifyController(BTCPayServerEnvironment btcPayServerEnvironment,
|
||||
IOptions<BTCPayServerOptions> btcPayServerOptions,
|
||||
IWebHostEnvironment webHostEnvironment,
|
||||
StoreRepository storeRepository,
|
||||
@ -55,7 +52,6 @@ namespace BTCPayServer.Plugins.Shopify
|
||||
IJsonHelper jsonHelper,
|
||||
IHttpClientFactory clientFactory)
|
||||
{
|
||||
_shopifyService = shopifyService;
|
||||
_btcPayServerEnvironment = btcPayServerEnvironment;
|
||||
_btcPayServerOptions = btcPayServerOptions;
|
||||
_webHostEnvironment = webHostEnvironment;
|
||||
@ -110,14 +106,14 @@ namespace BTCPayServer.Plugins.Shopify
|
||||
public async Task<IActionResult> ShopifyInvoiceEndpoint(
|
||||
string storeId, string orderId, decimal amount, bool checkOnly = false)
|
||||
{
|
||||
var shopifySearchTerm = $"{ShopifyService.SHOPIFY_ORDER_ID_PREFIX}{orderId}";
|
||||
var shopifySearchTerm = $"{ShopifyOrderMarkerHostedService.SHOPIFY_ORDER_ID_PREFIX}{orderId}";
|
||||
var matchedExistingInvoices = await _invoiceRepository.GetInvoices(new InvoiceQuery()
|
||||
{
|
||||
TextSearch = shopifySearchTerm,
|
||||
StoreId = new[] { storeId }
|
||||
});
|
||||
matchedExistingInvoices = matchedExistingInvoices.Where(entity =>
|
||||
entity.GetInternalTags(ShopifyService.SHOPIFY_ORDER_ID_PREFIX)
|
||||
entity.GetInternalTags(ShopifyOrderMarkerHostedService.SHOPIFY_ORDER_ID_PREFIX)
|
||||
.Any(s => s == orderId))
|
||||
.ToArray();
|
||||
|
||||
@ -159,7 +155,7 @@ namespace BTCPayServer.Plugins.Shopify
|
||||
if (client != null && order?.FinancialStatus == "pending" &&
|
||||
firstInvoiceSettled.Status != InvoiceStatusLegacy.Paid)
|
||||
{
|
||||
await _shopifyService.Process(client, orderId, firstInvoiceSettled.Id,
|
||||
await new OrderTransactionRegisterLogic(client).Process(orderId, firstInvoiceSettled.Id,
|
||||
firstInvoiceSettled.Currency,
|
||||
firstInvoiceSettled.Price.ToString(CultureInfo.InvariantCulture), true);
|
||||
order = await client.GetOrder(orderId);
|
||||
|
@ -70,47 +70,6 @@
|
||||
},
|
||||
"applicationUrl": "https://localhost:14142/"
|
||||
},
|
||||
"Altcoins": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"BTCPAY_EXPERIMENTALV2_CONFIRM": "true",
|
||||
"BTCPAY_NETWORK": "regtest",
|
||||
"BTCPAY_LAUNCHSETTINGS": "true",
|
||||
"BTCPAY_PORT": "14142",
|
||||
"BTCPAY_HttpsUseDefaultCertificate": "true",
|
||||
"BTCPAY_VERBOSE": "true",
|
||||
"BTCPAY_LTCEXPLORERURL": "http://127.0.0.1:32838/",
|
||||
"BTCPAY_LBTCEXPLORERURL": "http://127.0.0.1:32838/",
|
||||
"BTCPAY_BTCLIGHTNING": "type=clightning;server=tcp://127.0.0.1:30993/",
|
||||
"BTCPAY_BTCEXTERNALLNDREST": "type=lnd-rest;server=http://lnd:lnd@127.0.0.1:35531/;allowinsecure=true",
|
||||
"BTCPAY_BTCEXTERNALLNDSEEDBACKUP": "../BTCPayServer.Tests/TestData/LndSeedBackup/walletunlock.json",
|
||||
"BTCPAY_BTCEXTERNALSPARK": "server=/spark/btc/;cookiefile=fake",
|
||||
"BTCPAY_BTCEXTERNALCHARGE": "server=https://127.0.0.1:53280/mycharge/btc/;cookiefilepath=fake",
|
||||
"BTCPAY_BTCEXTERNALRTL": "server=/rtl/api/authenticate/cookie;cookiefile=fake",
|
||||
"BTCPAY_BTCEXTERNALTHUNDERHUB": "server=/thub/sso;cookiefile=fake",
|
||||
"BTCPAY_BTCEXTERNALTORQ": "server=/torq/cookie-login;cookiefile=fake",
|
||||
"BTCPAY_EXTERNALSERVICES": "totoservice:totolink;Lightning Terminal:/lit/;",
|
||||
"BTCPAY_EXTERNALCONFIGURATOR": "passwordfile=testpwd;server=/configurator",
|
||||
"BTCPAY_BTCEXPLORERURL": "http://127.0.0.1:32838/",
|
||||
"BTCPAY_ALLOW-ADMIN-REGISTRATION": "true",
|
||||
"BTCPAY_DISABLE-REGISTRATION": "false",
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"BTCPAY_CHAINS": "btc,ltc,lbtc,xmr",
|
||||
"BTCPAY_POSTGRES": "User ID=postgres;Include Error Detail=true;Host=127.0.0.1;Port=39372;Database=btcpayserver",
|
||||
"BTCPAY_DEBUGLOG": "debug.log",
|
||||
"BTCPAY_TORRCFILE": "../BTCPayServer.Tests/TestData/Tor/torrc",
|
||||
"BTCPAY_SOCKSENDPOINT": "localhost:9050",
|
||||
"BTCPAY_DOCKERDEPLOYMENT": "true",
|
||||
"BTCPAY_RECOMMENDED-PLUGINS": "",
|
||||
"BTCPAY_CHEATMODE": "true",
|
||||
"BTCPAY_EXPLORERPOSTGRES": "User ID=postgres;Include Error Detail=true;Host=127.0.0.1;Port=39372;Database=nbxplorer",
|
||||
"BTCPAY_XMR_DAEMON_URI": "http://127.0.0.1:18081",
|
||||
"BTCPAY_XMR_WALLET_DAEMON_URI": "http://127.0.0.1:18082",
|
||||
"BTCPAY_XMR_WALLET_DAEMON_WALLETDIR": "/path/to/monero_wallet"
|
||||
},
|
||||
"applicationUrl": "https://localhost:14142/"
|
||||
},
|
||||
"Altcoins-HTTPS": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
@ -136,7 +95,7 @@
|
||||
"BTCPAY_ALLOW-ADMIN-REGISTRATION": "true",
|
||||
"BTCPAY_DISABLE-REGISTRATION": "false",
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"BTCPAY_CHAINS": "btc,ltc,lbtc,xmr",
|
||||
"BTCPAY_CHAINS": "btc,ltc,lbtc",
|
||||
"BTCPAY_POSTGRES": "User ID=postgres;Include Error Detail=true;Host=127.0.0.1;Port=39372;Database=btcpayserver",
|
||||
"BTCPAY_SSHCONNECTION": "root@127.0.0.1:21622",
|
||||
"BTCPAY_SSHPASSWORD": "opD3i2282D",
|
||||
@ -146,10 +105,7 @@
|
||||
"BTCPAY_DOCKERDEPLOYMENT": "true",
|
||||
"BTCPAY_RECOMMENDED-PLUGINS": "",
|
||||
"BTCPAY_CHEATMODE": "true",
|
||||
"BTCPAY_EXPLORERPOSTGRES": "User ID=postgres;Include Error Detail=true;Host=127.0.0.1;Port=39372;Database=nbxplorer",
|
||||
"BTCPAY_XMR_DAEMON_URI": "http://127.0.0.1:18081",
|
||||
"BTCPAY_XMR_WALLET_DAEMON_URI": "http://127.0.0.1:18082",
|
||||
"BTCPAY_XMR_WALLET_DAEMON_WALLETDIR": "/path/to/monero_wallet"
|
||||
"BTCPAY_EXPLORERPOSTGRES": "User ID=postgres;Include Error Detail=true;Host=127.0.0.1;Port=39372;Database=nbxplorer"
|
||||
},
|
||||
"applicationUrl": "https://localhost:14142/"
|
||||
}
|
||||
|
@ -9,8 +9,7 @@ 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;
|
||||
|
||||
@ -28,18 +27,12 @@ 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);
|
||||
// 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();
|
||||
|
||||
var val = splitted.FirstOrDefault(a => a.IndexOf(ValueSeparator, StringComparison.OrdinalIgnoreCase) == -1);
|
||||
TextSearch = val != null ? val.Trim() : string.Empty;
|
||||
}
|
||||
|
||||
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; }
|
||||
|
||||
@ -89,10 +82,9 @@ namespace BTCPayServer
|
||||
|
||||
public string WithoutSearchText()
|
||||
{
|
||||
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();
|
||||
return string.IsNullOrEmpty(TextSearch)
|
||||
? Finalize(ToString())
|
||||
: Finalize(ToString()).Replace(TextSearch, string.Empty);
|
||||
}
|
||||
|
||||
public string[] GetFilterArray(string key)
|
||||
@ -152,7 +144,7 @@ namespace BTCPayServer
|
||||
|
||||
private static string Finalize(string str)
|
||||
{
|
||||
var value = str.Trim().TrimStart(FilterSeparator).TrimEnd(FilterSeparator);
|
||||
var value = str.TrimStart(FilterSeparator).TrimEnd(FilterSeparator);
|
||||
return string.IsNullOrEmpty(value) ? " " : value;
|
||||
}
|
||||
}
|
||||
|
@ -249,28 +249,10 @@ namespace BTCPayServer.Services.Altcoins.Monero.UI
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var response = await _MoneroRpcProvider.WalletRpcClients[cryptoCode].SendCommandAsync<OpenWalletRequest, OpenWalletResponse>("open_wallet", new OpenWalletRequest
|
||||
{
|
||||
Filename = "wallet",
|
||||
Password = viewModel.WalletPassword
|
||||
});
|
||||
if (response?.Error != null)
|
||||
{
|
||||
throw new Exception(response.Error.Message);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModelState.AddModelError(nameof(viewModel.AccountIndex), $"Could not open the wallet: {ex.Message}");
|
||||
return View(viewModel);
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(GetStoreMoneroLikePaymentMethod), new
|
||||
{
|
||||
cryptoCode,
|
||||
StatusMessage = "View-only wallet files uploaded. The wallet will soon become available."
|
||||
StatusMessage = "View-only wallet files uploaded. If they are valid the wallet will soon become available."
|
||||
|
||||
});
|
||||
}
|
||||
|
@ -1,133 +0,0 @@
|
||||
#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.FirstOrDefault(item =>
|
||||
var overrideLink = links.SingleOrDefault(item =>
|
||||
item.CryptoCode.Equals(pmi.CryptoCode, StringComparison.InvariantCultureIgnoreCase) ||
|
||||
item.CryptoCode.Equals(pmi.ToString(), StringComparison.InvariantCultureIgnoreCase));
|
||||
prov.OverrideBlockExplorerLink = overrideLink?.Link ?? prov.BlockExplorerLinkDefault;
|
||||
|
@ -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.Context.RegisterClassMap<ExportTransactionMap>();
|
||||
csvWriter.Configuration.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).Convert(row => row.Value.Labels == null ? string.Empty : string.Join(", ", row.Value.Labels));
|
||||
Map(m => m.Labels).ConvertUsing(row => row.Labels == null ? string.Empty : string.Join(", ", row.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/" target="_blank" rel="noreferrer noopener" title="More information...">
|
||||
<a href="https://docs.btcpayserver.org/Development/Theme/#2-bootstrap-themes" target="_blank" rel="noreferrer noopener" title="More information...">
|
||||
<vc:icon symbol="info" />
|
||||
</a>
|
||||
<input asp-for="CustomCSSLink" class="form-control" />
|
||||
|
@ -1,7 +1,6 @@
|
||||
@using BTCPayServer.Client
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@using BTCPayServer.Abstractions.TagHelpers
|
||||
@using BTCPayServer.Models.ServerViewModels
|
||||
@model BTCPayServer.Models.EmailsViewModel
|
||||
|
||||
<div class="row">
|
||||
@ -45,7 +44,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="@(Model is ServerEmailsViewModel ? Policies.CanModifyServerSettings : Policies.CanModifyStoreSettings)">
|
||||
<div class="form-group" permission="@Policies.CanModifyStoreSettings">
|
||||
@if (!Model.PasswordSet)
|
||||
{
|
||||
<label asp-for="Settings.Password" class="form-label"></label>
|
||||
@ -82,6 +81,7 @@
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
|
||||
delegate('click', '#quick-fill .dropdown-menu a', function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
|
@ -72,7 +72,6 @@ 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/" target="_blank" rel="noreferrer noopener" title="More information...">
|
||||
<a href="https://docs.btcpayserver.org/Development/Theme/#2-bootstrap-themes" target="_blank" rel="noreferrer noopener" title="More information...">
|
||||
<vc:icon symbol="info" />
|
||||
</a>
|
||||
<input asp-for="CustomCSSLink" class="form-control" />
|
||||
|
@ -1,4 +1,3 @@
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@model (Dictionary<string, object> Items, int Level)
|
||||
|
||||
@functions {
|
||||
@ -11,20 +10,14 @@
|
||||
|
||||
@if (Model.Items.Any())
|
||||
{
|
||||
@* Use titlecase and lowercase versions for backwards-compatibility *@
|
||||
string[] cartKeys = ["cart", "subtotal", "discount", "tip", "total"];
|
||||
var hasCart = Model.Items.ContainsKey("Cart");
|
||||
<table class="table my-0" v-pre>
|
||||
@if (Model.Items.Keys.Any(key => cartKeys.Contains(key.ToLowerInvariant())))
|
||||
@if (hasCart || (Model.Items.ContainsKey("Subtotal") && Model.Items.ContainsKey("Total")))
|
||||
{
|
||||
_ = 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)
|
||||
@if (hasCart)
|
||||
{
|
||||
<tbody>
|
||||
@foreach (var (key, value) in cartDict)
|
||||
@foreach (var (key, value) in (Dictionary<string, object>)Model.Items["Cart"])
|
||||
{
|
||||
<tr>
|
||||
<th>@key</th>
|
||||
@ -33,46 +26,35 @@
|
||||
}
|
||||
</tbody>
|
||||
}
|
||||
else if (cart is ICollection<object> { Count: > 0 } cartCollection)
|
||||
{
|
||||
<tbody>
|
||||
@foreach (var value in cartCollection)
|
||||
<tfoot style="border-top-width:@(hasCart ? "3px" : "0")">
|
||||
@if (Model.Items.ContainsKey("Subtotal"))
|
||||
{
|
||||
<tr>
|
||||
<td>@value</td>
|
||||
<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>
|
||||
</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
|
||||
|
@ -18,11 +18,11 @@
|
||||
@RenderBody()
|
||||
</div>
|
||||
</section>
|
||||
@if (ViewData["ShowFooter"] is not false)
|
||||
{
|
||||
<partial name="_Footer"/>
|
||||
}
|
||||
<partial name="LayoutFoot" />
|
||||
@await RenderSectionAsync("PageFootContent", false)
|
||||
@if (ViewData["ShowFooter"] is not false)
|
||||
{
|
||||
<partial name="_Footer"/>
|
||||
}
|
||||
<partial name="LayoutFoot" />
|
||||
@await RenderSectionAsync("PageFootContent", false)
|
||||
</body>
|
||||
</html>
|
||||
|
@ -52,7 +52,6 @@
|
||||
</button>
|
||||
</nav>
|
||||
<section id="payment" v-if="isActive">
|
||||
<div v-if="srvModel.itemDesc && srvModel.itemDesc !== srvModel.storeName" v-text="srvModel.itemDesc" class="fw-semibold text-center text-muted mb-3"></div>
|
||||
<div class="d-flex justify-content-center mt-1 text-center">
|
||||
@if (Model.IsUnsetTopUp)
|
||||
{
|
||||
|
@ -431,7 +431,7 @@
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
@if (Model.ReceiptData?.Any() is true)
|
||||
@if (Model.ReceiptData != null && Model.ReceiptData.Any())
|
||||
{
|
||||
<div>
|
||||
<h3 class="mb-3">
|
||||
@ -443,7 +443,7 @@
|
||||
<partial name="PosData" model="(Model.ReceiptData, 1)" />
|
||||
</div>
|
||||
}
|
||||
@if (Model.AdditionalData?.Any() is true)
|
||||
@if (Model.AdditionalData != null && Model.AdditionalData.Any())
|
||||
{
|
||||
<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; }
|
||||
#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; }
|
||||
#posData td > table:last-child { margin-bottom: 0 !important; }
|
||||
#posData 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 data="@Context.Request.GetCurrentUrl()"></vc:qr-code>
|
||||
}
|
||||
<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" id="ReceiptLinkPrint">Print</a>
|
||||
<a href="?print=true" class="flex-grow-0 align-self-start btn btn-secondary d-print-none fs-4" target="_blank">Print</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@ -102,15 +102,6 @@
|
||||
</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">
|
||||
@ -173,9 +164,9 @@
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(Model.RedirectUrl))
|
||||
@if (!string.IsNullOrEmpty(Model.OrderUrl))
|
||||
{
|
||||
<a href="@Model.RedirectUrl" class="btn btn-secondary rounded-pill mx-auto mt-3" rel="noreferrer noopener" target="_blank">Return to @(string.IsNullOrEmpty(Model.StoreName) ? "store" : Model.StoreName)</a>
|
||||
<a href="@Model.OrderUrl" class="btn btn-secondary rounded-pill mx-auto mt-3" rel="noreferrer noopener" target="_blank">Return to @(string.IsNullOrEmpty(Model.StoreName) ? "store" : Model.StoreName)</a>
|
||||
}
|
||||
</div>
|
||||
</main>
|
||||
|
@ -1,17 +1,4 @@
|
||||
@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
|
||||
@ -103,7 +90,6 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
var hasCart = Model.CartData?.Any() is true;
|
||||
<div id="PaymentDetails">
|
||||
<div class="my-2 text-center small">
|
||||
@if (!string.IsNullOrEmpty(Model.OrderId))
|
||||
@ -113,92 +99,53 @@
|
||||
@Model.Timestamp.ToBrowserDate()
|
||||
</div>
|
||||
<table class="table table-borderless table-sm small my-0">
|
||||
@if (Model.AdditionalData?.Any() is true)
|
||||
<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")))
|
||||
{
|
||||
@foreach (var (key, value) in Model.AdditionalData)
|
||||
@if (Model.AdditionalData.ContainsKey("Cart"))
|
||||
{
|
||||
<tr class="additional-data">
|
||||
<td class="text-secondary">@key</td>
|
||||
<td class="text-end">@value</td>
|
||||
@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>
|
||||
}
|
||||
<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];
|
||||
@ -207,33 +154,33 @@
|
||||
<tr>
|
||||
<td colspan="2" class="text-nowrap text-secondary">Payment @(i + 1)</td>
|
||||
</tr>
|
||||
<tr class="payment-data">
|
||||
<tr>
|
||||
<td class="text-nowrap">Received</td>
|
||||
<td>@payment.ReceivedDate.ToBrowserDate()</td>
|
||||
</tr>
|
||||
}
|
||||
<tr class="payment-data">
|
||||
<tr>
|
||||
<td class="text-nowrap text-secondary">@(Model.Payments.Count == 1 ? "Paid" : "")</td>
|
||||
<td class="text-end">@payment.AmountFormatted</td>
|
||||
</tr>
|
||||
<tr class="payment-data">
|
||||
<tr>
|
||||
<td colspan="2" class="text-end">@payment.PaidFormatted</td>
|
||||
</tr>
|
||||
<tr class="payment-data">
|
||||
<tr>
|
||||
<td class="text-nowrap text-secondary">Rate</td>
|
||||
<td class="text-end">@payment.RateFormatted</td>
|
||||
</tr>
|
||||
@if (!string.IsNullOrEmpty(payment.Destination))
|
||||
{
|
||||
<tr class="payment-data">
|
||||
<tr>
|
||||
<td class="text-nowrap text-secondary">Destination</td>
|
||||
<td class="text-break">
|
||||
@if (payment.Destination.Length > 69)
|
||||
{
|
||||
<span>
|
||||
<span>@payment.Destination[..19]</span>
|
||||
<span>@payment.Destination[..30]</span>
|
||||
<span>...</span>
|
||||
<span>@payment.Destination.Substring(payment.Destination.Length - 20, 20)</span>
|
||||
<span>@payment.Destination.Substring(payment.Destination.Length - 30, 30)</span>
|
||||
</span>
|
||||
}
|
||||
else
|
||||
@ -245,7 +192,7 @@
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(payment.PaymentProof))
|
||||
{
|
||||
<tr class="payment-data">
|
||||
<tr>
|
||||
<td class="text-nowrap text-secondary">Pay Proof</td>
|
||||
<td class="text-break">@payment.PaymentProof</td>
|
||||
</tr>
|
||||
@ -269,9 +216,7 @@
|
||||
<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,6 +3,7 @@
|
||||
@model BTCPayServer.Services.Altcoins.Monero.UI.UIMoneroLikeStoreController.MoneroLikePaymentMethodListViewModel
|
||||
|
||||
@{
|
||||
Layout = "../Shared/_NavLayout.cshtml";
|
||||
ViewData.SetActivePage("Monero Settings", "Monero Settings", "Monero Settings");
|
||||
ViewData["NavPartialName"] = "../UIStores/_Nav";
|
||||
}
|
||||
|
@ -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/" target="_blank" rel="noreferrer noopener" title="More information...">
|
||||
<a href="https://docs.btcpayserver.org/Development/Theme/#2-bootstrap-themes" target="_blank" rel="noreferrer noopener" title="More information...">
|
||||
<vc:icon symbol="info" />
|
||||
</a>
|
||||
<input asp-for="CustomCSSLink" class="form-control" />
|
||||
|
@ -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/" target="_blank" rel="noreferrer noopener">
|
||||
<a href="https://docs.btcpayserver.org/Development/Theme/#2-bootstrap-themes" 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">
|
||||
<vc:icon symbol="scan-qr"/>
|
||||
<i class="fa fa-camera"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -202,7 +202,7 @@
|
||||
</p>
|
||||
@if (Model.LnurlEndpoint is not null)
|
||||
{
|
||||
<p id="BoltcardActions">
|
||||
<p id="BoltcardActions" style="visibility:hidden">
|
||||
<a id="SetupBoltcard" asp-action="SetupBoltcard" asp-controller="UIPullPayment" asp-route-pullPaymentId="@Model.Id" asp-route-command="configure-boltcard">
|
||||
Setup Boltcard
|
||||
</a>
|
||||
@ -229,10 +229,11 @@
|
||||
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,7 +20,6 @@
|
||||
#app .table-responsive { max-height: 80vh; }
|
||||
#app #charts { gap: var(--btcpay-space-l) var(--btcpay-space-xxl); }
|
||||
#app #charts article { flex: 1 1 450px; }
|
||||
main .dropdown-menu.show { z-index: 99999; }
|
||||
</style>
|
||||
}
|
||||
|
||||
@ -33,7 +32,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 data</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 date</a>
|
||||
<button id="exportCSV" class="btn btn-primary text-nowrap" type="button">Export</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -134,7 +133,7 @@
|
||||
<template v-else-if="srv.result.fields[columnIndex].type === 'tx_id'">
|
||||
<vc:truncate-center text="value" is-vue="true" padding="15" classes="truncate-center-id" link="getExplorerUrl(value, row[columnIndex-1])" />
|
||||
</template>
|
||||
<template v-else-if="value && ['Address'].includes(srv.result.fields[columnIndex].name)" >
|
||||
<template v-else-if="value && ['Address', 'PaymentId'].includes(srv.result.fields[columnIndex].name)" >
|
||||
<vc:truncate-center text="value" is-vue="true" padding="15" classes="truncate-center-id" />
|
||||
</template>
|
||||
<template v-else-if="srv.result.fields[columnIndex].type === 'datetime'">{{ displayDate(value) }}</template>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user